[Sugar-devel] [PATCH sugar v8] sl#3317: Batch Operations on Journal Entries (Copy, Erase)

Ajay Garg ajay at activitycentral.com
Tue Feb 21 04:08:43 EST 2012


Note that this patch must be appled in full, and not over version-1, version-2,
version-3, version-4, version-5, version-6, or version-7 patch.

Also note that this patch MUST be applied after the applying of patch at :
http://patchwork.sugarlabs.org/patch/1157/


== This code has been written almost exclusively by Martin Abente.

== Design discussions at :
  http://wiki.sugarlabs.org/go/Features/Multi_selection

== Screenshots at :
  http://wiki.sugarlabs.org/go/Features/Multi_selection_screenshots

== Martin's work's video at :
  http://www.sugarlabs.org/~tch/journal2.mpeg



Following are the changes/enhancements from Martin's work :

a. More copy-to options :: Clipboard, Documents (in addition to mounted drives).

b. After entries are copied to another location, both - the source and the target - entries
  are de-selected automatically, without the user explicitly have to de-select them all manually.

c. There has been a progress bar added for batch-operations.


Codewise, effore has been put to have maximum code-reuse; and mimimal code-duplication,
as most of the operation-parts are same, irrespective of the operation performed.


=================================
CHANGELOG
=================================

Changes of version-2 over version-1
-----------------------------------

Fixed the following :

a. Let's say, there is a saved instance of "Ruler" activity in the
journal, among other possible entries. ("Ruler" is not a special one.
In fact, any activity which in single-mode would display "Entries
without a file cannot be copied" would work).

b. Do "Select All".

c. When "Ruler" gets selected, the message "Error: Entries without a
   file cannot be copied" is displayed (at the select-all stage).

EXPECTED BEHAVIOUR ::

a. This message should not be displayed at the time of selection.
   Rather, it should be displayed at the time of "Copy-all" operation.


======================================================================

Changes of version-3 over version-2
-----------------------------------

Solved miscellaneous issues (courtesy Anish and Daniel).

A. Sometimes, after doing select-none, the toolbar did not change back
   to the normal, non-editing mode.

REASONS
--------

a. The count of selected-entries were being updated, only if the checkboxes were toggled
   individually, but not during batch-operations.

b. Also somewhat related, if the user changed to alternate volume-buttons, the
   edittoolbox would not be updated everytime. Now, on second thoughts, it seems that
   this switching itself should not be allowed, as random switching to different
   volumes (with some selected entries still "un-operated") will only make things messy.

    
FIXES:
------

a. Corrected the count of checkbox changes, both during single-mode and batch-mode.

b. As long as there are some (at least one) selected entries on a particular volume,
   rest of the volumetoolbar buttons (in the bottom panel) will be made de-sensitive
   (that is, no switching to any other volume will be allowed, until all entries on the 
   current volume are de-selected, either via indivual deselect; or batch-select; or 
   operation-completion).

--------------------------------------------------------------------------


B. Now, the status info-alert (of the entries selected/deselected) is shown AFTER
   the select-all, and select-none operations have finished.


---------------------------------------------------------------------------


C. Re-coded alerts. Now, there is only alert each (for the singleton journal activity instance)
   of the following types ::

   (i)   Info-alert, which shows the running status; the new status text is simply replaced
         (earlier a whole new alert was being generated, resulting in flickering).
   (ii)  Error-alert, which is "info-alert" plus a "OK" button. Here too, the text is simply
         updated, and the corresponding callback called upon clicking "OK" button.
   (iii) Confirmation-alert, which is "Error-alert" plus a "Cancel" button.


------------------------------------------------------------------------------

D. Adding clock-cursor during batch-operations.


-------------------------------------------------------------------------------


======================================================================

Changes of version-4 over version-3
-----------------------------------

A. Fixed the regression - "Copy-to" options not available in "View Details" of
   a journal entry.
   Thanks Anish for catching that.

B. Fixed the regression - "Duplicate" not available in "View Details" of a 
   journal entry.
   Thanks Anish for catching that.

C. Speeded-up batch operations. This has been achieved by inhibiting journal-refresh 
   during each metadata-processing in batch operations. 
   The fix is; now, at the start of the batch operation, refresh is inhibited. After the 
   batch-operation is completed, refresh is allowed back. This results in only one-refresh-
   per-action, rather than one-refresh-per-metadata-per-action.

   Thanks Daniel for providing me the motivation.



=======================================================================

Changes of version-5 over version-4
-----------------------------------

A. Temporary workaround for the dbus-timeout issue. Enclosed 
   metadata-copy, and metadata-write operations in a Try/Catch block,
   so that ANY exceptions/errors are caught; while the operation
   continues to proceeed.
   Thanks Anish for catching that.



=======================================================================

Changes of version-6 over version-5
-----------------------------------

A. Any journal-entries with ".xo" extension, were being installed
   even though it was only being selected/deselected (in both 
   single- and double- mode). Fixed it.
   Thanks Anish and Sascha.



=======================================================================

Changes of version-7 over version-6
-----------------------------------

A. Select-all/Select-None actions have been made in-memory. This
   serves the following two purposes ::
 
   (i)  Now, the select/deselect actions are not persisted to disk.
   (ii) Major speedup in select/deselect operations.

   The solution was figured out by Anish; I just did the mechanics of
   coding, that's it ;)

   Also, thanks to Sascha.



=======================================================================

Changes of version-8 over version-7
-----------------------------------

A. A minor change in "__select_set_data_cb(self, column, cell, tree_model, tree_iter)"
   in class "listview.py".

   This UI callback function may be called, when the model is
   still not ready.
   Since this function just affects the checkbox (only a UI
   change), so if the model is still not ready, it is safe to
   return at this point.

   Thanks to Anish for catching that.



 src/jarabe/journal/journalactivity.py |  139 +++++++-
 src/jarabe/journal/journaltoolbox.py  |  238 ++++++++++--
 src/jarabe/journal/listmodel.py       |   27 ++
 src/jarabe/journal/listview.py        |   99 +++++
 src/jarabe/journal/model.py           |   13 +-
 src/jarabe/journal/palettes.py        |  686 +++++++++++++++++++++++++++------
 src/jarabe/journal/volumestoolbar.py  |    9 +
 7 files changed, 1054 insertions(+), 157 deletions(-)

diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index 8cafef0..2e044fc 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -1,5 +1,9 @@
 # Copyright (C) 2006, Red Hat, Inc.
 # Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2012, Walter Bender  <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente  <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg      <ajay at activitycentral.com>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -17,15 +21,18 @@
 
 import logging
 from gettext import gettext as _
+from gettext import ngettext
 import uuid
 
 import gtk
 import dbus
 import statvfs
 import os
+import gobject
 
 from sugar.graphics.window import Window
-from sugar.graphics.alert import ErrorAlert
+from sugar.graphics.alert import Alert, ErrorAlert, ConfirmationAlert
+from sugar.graphics.icon import Icon
 
 from sugar.bundle.bundle import ZipExtractException, RegistrationException
 from sugar import env
@@ -34,7 +41,9 @@ from sugar import wm
 
 from jarabe.model import bundleregistry
 from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.journaltoolbox import EditToolbox
 from jarabe.journal.listview import ListView
+from jarabe.journal.listmodel import ListModel
 from jarabe.journal.detailview import DetailView
 from jarabe.journal.volumestoolbar import VolumesToolbar
 from jarabe.journal import misc
@@ -53,6 +62,7 @@ _SPACE_TRESHOLD = 52428800
 _BUNDLE_ID = 'org.laptop.JournalActivity'
 
 _journal = None
+_mount_point = None
 
 
 class JournalActivityDBusService(dbus.service.Object):
@@ -119,8 +129,21 @@ class JournalActivity(JournalWindow):
         self._list_view = None
         self._detail_view = None
         self._main_toolbox = None
+        self._edit_toolbox = None
         self._detail_toolbox = None
         self._volumes_toolbar = None
+        self._editing_mode = False
+        self._alert = Alert()
+        self._error_alert = ErrorAlert()
+        self._confirmation_alert = ConfirmationAlert()
+        self._current_alert = None
+        self.setup_handlers_for_alert_actions()
+
+        self._info_alert = None
+        self._selected_entries = []
+        self._bundle_installation_allowed = True
+
+        set_mount_point('/')
 
         self._setup_main_view()
         self._setup_secondary_view()
@@ -151,6 +174,9 @@ class JournalActivity(JournalWindow):
         self.add_alert(alert)
         alert.show()
 
+    def _volume_error_cb(self, gobject, message, severity):
+        self.update_error_alert(severity, message, None, None)
+
     def __alert_response_cb(self, alert, response_id):
         self.remove_alert(alert)
 
@@ -184,7 +210,7 @@ class JournalActivity(JournalWindow):
         search_toolbar = self._main_toolbox.search_toolbar
         search_toolbar.connect('query-changed', self._query_changed_cb)
         search_toolbar.set_mount_point('/')
-        self._mount_point = '/'
+        set_mount_point('/')
 
     def _setup_secondary_view(self):
         self._secondary_view = gtk.VBox()
@@ -217,9 +243,13 @@ class JournalActivity(JournalWindow):
         self.show_main_view()
 
     def show_main_view(self):
-        if self.toolbar_box != self._main_toolbox:
-            self.set_toolbar_box(self._main_toolbox)
-            self._main_toolbox.show()
+        if self._editing_mode:
+            toolbox = EditToolbox()
+        else:
+            toolbox = self._main_toolbox
+
+        self.set_toolbar_box(toolbox)
+        toolbox.show()
 
         if self.canvas != self._main_view:
             self.set_canvas(self._main_view)
@@ -254,7 +284,7 @@ class JournalActivity(JournalWindow):
     def __volume_changed_cb(self, volume_toolbar, mount_point):
         logging.debug('Selected volume: %r.', mount_point)
         self._main_toolbox.search_toolbar.set_mount_point(mount_point)
-        self._mount_point = mount_point
+        set_mount_point(mount_point)
         self._main_toolbox.set_current_toolbar(0)
 
     def __model_created_cb(self, sender, **kwargs):
@@ -281,6 +311,9 @@ class JournalActivity(JournalWindow):
         self._list_view.update_dates()
 
     def _check_for_bundle(self, object_id):
+        if not self._bundle_installation_allowed:
+            return
+
         registry = bundleregistry.get_registry()
 
         metadata = model.get(object_id)
@@ -316,6 +349,9 @@ class JournalActivity(JournalWindow):
         metadata['bundle_id'] = bundle.get_bundle_id()
         model.write(metadata)
 
+    def set_bundle_installation_allowed(self, allowed):
+        self._bundle_installation_allowed = allowed
+
     def search_grab_focus(self):
         search_toolbar = self._main_toolbox.search_toolbar
         search_toolbar.give_entry_focus()
@@ -364,8 +400,87 @@ class JournalActivity(JournalWindow):
         self.show_main_view()
         self.search_grab_focus()
 
-    def get_mount_point(self):
-        return self._mount_point
+    def switch_to_editing_mode(self, switch):
+        # Toggle sensitivity of volume-toolbar buttons.
+        self._volumes_toolbar.set_volume_buttons_sensitive(not switch,
+                                                           get_mount_point())
+
+        # (re)-switch, only if not already.
+        if (switch) and (not self._editing_mode):
+            self._editing_mode = True
+            self.show_main_view()
+        elif (not switch) and (self._editing_mode):
+            self._editing_mode = False
+            self.show_main_view()
+
+    def get_list_view(self):
+        return self._list_view
+
+    def setup_handlers_for_alert_actions(self):
+        self._error_alert.connect('response',
+                                   self.__check_for_alert_action)
+        self._confirmation_alert.connect('response',
+                                   self.__check_for_alert_action)
+
+    def __check_for_alert_action(self, alert, response_id):
+        self.hide_alert()
+        if self._callback is not None:
+            if response_id == gtk.RESPONSE_OK:
+                gobject.idle_add(self._callback, self._data, True)
+
+    def update_title_and_message(self, alert, title, message):
+        alert.props.title = title
+        alert.props.msg = message
+
+    def update_alert(self, alert):
+        if self._current_alert is None:
+            self.add_alert(alert)
+        elif self._current_alert != alert:
+            self.remove_alert(self._current_alert)
+            self.add_alert(alert)
+
+        self._current_alert = alert
+        self._current_alert.show()
+
+    def hide_alert(self):
+        if self._current_alert is not None:
+            self._current_alert.hide()
+
+    def update_info_alert(self, title, message, callback, data):
+        self.update_title_and_message(self._alert, title, message)
+        self.update_alert(self._alert)
+        if callback is not None:
+            gobject.idle_add(callback, data)
+
+    def update_error_alert(self, title, message, callback, data):
+        self.update_title_and_message(self._error_alert, title,
+                                       message)
+        self._callback = callback
+        self._data = data
+        self.update_alert(self._error_alert)
+
+    def update_confirmation_alert(self, title, message, callback,
+                                  data):
+        self.update_title_and_message(self._confirmation_alert, title,
+                                       message)
+        self._callback = callback
+        self._data = data
+        self.update_alert(self._confirmation_alert)
+
+    def get_metadata_list(self, selected_state):
+        metadata_list = []
+
+        list_view_model = self.get_list_view().get_model()
+        for index in range(0, len(list_view_model)):
+            metadata = list_view_model.get_metadata(index)
+            metadata_selected = \
+                    list_view_model.get_selected_value(metadata['uid'])
+
+            if ( (selected_state and metadata_selected) or \
+                    ((not selected_state) and (not metadata_selected)) ):
+                metadata_list.append(metadata)
+
+        return metadata_list
 
 
 def get_journal():
@@ -378,3 +493,11 @@ def get_journal():
 
 def start():
     get_journal()
+
+
+def set_mount_point(mount_point):
+    global _mount_point
+    _mount_point = mount_point
+
+def get_mount_point():
+    return _mount_point
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index 2aa4153..2db9f46 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -1,5 +1,9 @@
 # Copyright (C) 2007, One Laptop Per Child
 # Copyright (C) 2009, Walter Bender
+# Copyright (C) 2012, Walter Bender  <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente  <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg      <ajay at activitycentral.com>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -16,6 +20,7 @@
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
 from gettext import gettext as _
+from gettext import ngettext
 import logging
 from datetime import datetime, timedelta
 import os
@@ -43,8 +48,9 @@ from sugar import mime
 from jarabe.model import bundleregistry
 from jarabe.journal import misc
 from jarabe.journal import model
-from jarabe.journal.palettes import ClipboardMenu
-from jarabe.journal.palettes import VolumeMenu
+from jarabe.journal import palettes
+
+COPY_MENU_HELPER = palettes.get_copy_menu_helper()
 
 
 _AUTOSEARCH_TIMEOUT = 1000
@@ -455,39 +461,11 @@ class EntryToolbar(gtk.Toolbar):
             palette.menu.remove(menu_item)
             menu_item.destroy()
 
-        clipboard_menu = ClipboardMenu(self._metadata)
-        clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
-                                      icon_size=gtk.ICON_SIZE_MENU))
-        clipboard_menu.connect('volume-error', self.__volume_error_cb)
-        palette.menu.append(clipboard_menu)
-        clipboard_menu.show()
-
-        if self._metadata['mountpoint'] != '/':
-            client = gconf.client_get_default()
-            color = XoColor(client.get_string('/desktop/sugar/user/color'))
-            journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
-            journal_menu.set_image(Icon(icon_name='activity-journal',
-                                        xo_color=color,
-                                        icon_size=gtk.ICON_SIZE_MENU))
-            journal_menu.connect('volume-error', self.__volume_error_cb)
-            palette.menu.append(journal_menu)
-            journal_menu.show()
-
-        volume_monitor = gio.volume_monitor_get()
-        icon_theme = gtk.icon_theme_get_default()
-        for mount in volume_monitor.get_mounts():
-            if self._metadata['mountpoint'] == mount.get_root().get_path():
-                continue
-            volume_menu = VolumeMenu(self._metadata, mount.get_name(),
-                                     mount.get_root().get_path())
-            for name in mount.get_icon().props.names:
-                if icon_theme.has_icon(name):
-                    volume_menu.set_image(Icon(icon_name=name,
-                                               icon_size=gtk.ICON_SIZE_MENU))
-                    break
-            volume_menu.connect('volume-error', self.__volume_error_cb)
-            palette.menu.append(volume_menu)
-            volume_menu.show()
+        COPY_MENU_HELPER.insert_copy_to_menu_items(palette.menu,
+                                                   [self._metadata],
+                                                   show_editing_alert=False,
+                                                   show_progress_info_alert=False,
+                                                   batch_mode=False)
 
     def _refresh_duplicate_palette(self):
         color = misc.get_icon_color(self._metadata)
@@ -527,6 +505,196 @@ class EntryToolbar(gtk.Toolbar):
             menu_item.show()
 
 
+class EditToolbox(Toolbox):
+    def __init__(self):
+        Toolbox.__init__(self)
+
+        self.edit_toolbar = EditToolbar()
+        self.add_toolbar('', self.edit_toolbar)
+        self.edit_toolbar.show()
+
+
+class EditToolbar(gtk.Toolbar):
+    def __init__(self):
+        gtk.Toolbar.__init__(self)
+
+        self.add(SelectNoneButton())
+        self.add(SelectAllButton())
+        self.add(gtk.SeparatorToolItem())
+        self.add(BatchEraseButton())
+        self.add(BatchCopyButton())
+
+        self.show_all()
+
+
+class SelectNoneButton(ToolButton, palettes.ActionItem):
+    def __init__(self):
+        ToolButton.__init__(self, 'select-none')
+        palettes.ActionItem.__init__(self, '', [],
+                                     show_editing_alert=False,
+                                     show_progress_info_alert=True,
+                                     batch_mode=True,
+                                     need_to_popup_options=False,
+                                     operate_on_deselected_entries=False,
+                                     switch_to_normal_mode_after_completion=True,
+                                     show_post_selected_confirmation=True,
+                                     show_not_completed_ops_info=False)
+        self.props.tooltip = _('Select none')
+
+    def _get_actionable_signal(self):
+        return 'clicked'
+
+    def _get_editing_alert_operation(self):
+        return _('Select None')
+
+    def _get_info_alert_title(self):
+        return _('Deselecting')
+
+    def _get_post_selection_alert_message_entries_len(self):
+        return self._metadata_list_initial_len
+
+    def _get_post_selection_alert_message(self, entries_len):
+        return ngettext('You have deselected %d entry.',
+                        'You have deselected %d entries.',
+                        entries_len) % (entries_len,)
+
+    def _operate(self, metadata):
+        # Nothing specific needs to be done.
+        # The checkboxes are unchecked as part of the toggling of any
+        # operation that operates on selected entries.
+
+        # This is sync-operation. Thus, call the callback.
+        self._post_operate_per_metadata_per_action(metadata)
+
+
+class SelectAllButton(ToolButton, palettes.ActionItem):
+    def __init__(self):
+        ToolButton.__init__(self, 'select-all')
+        palettes.ActionItem.__init__(self, '', [],
+                                     show_editing_alert=False,
+                                     show_progress_info_alert=True,
+                                     batch_mode=True,
+                                     need_to_popup_options=False,
+                                     operate_on_deselected_entries=True,
+                                     switch_to_normal_mode_after_completion=False,
+                                     show_post_selected_confirmation=True,
+                                     show_not_completed_ops_info=False)
+        self.props.tooltip = _('Select all')
+
+    def _get_actionable_signal(self):
+        return 'clicked'
+
+    def _get_editing_alert_operation(self):
+        return _('Select All')
+
+    def _get_info_alert_title(self):
+        return _('Selecting')
+
+    def _get_post_selection_alert_message_entries_len(self):
+        return self._model_len
+
+    def _get_post_selection_alert_message(self, entries_len):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        return ngettext('You have selected %d entry.',
+                        'You have selected %d entries.',
+                         entries_len) % (entries_len,)
+
+    def _operate(self, metadata):
+        # Nothing specific needs to be done.
+        # The checkboxes are unchecked as part of the toggling of any
+        # operation that operates on selected entries.
+
+        # This is sync-operation. Thus, call the callback.
+        self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchEraseButton(ToolButton, palettes.ActionItem):
+    def __init__(self):
+        ToolButton.__init__(self, 'edit-delete')
+        palettes.ActionItem.__init__(self, '', [],
+                                     show_editing_alert=True,
+                                     show_progress_info_alert=True,
+                                     batch_mode=True,
+                                     need_to_popup_options=False,
+                                     operate_on_deselected_entries=False,
+                                     switch_to_normal_mode_after_completion=True,
+                                     show_post_selected_confirmation=False,
+                                     show_not_completed_ops_info=True)
+        self.props.tooltip = _('Erase')
+
+    def _get_actionable_signal(self):
+        return 'clicked'
+
+    def _get_editing_alert_title(self):
+        return _('Erase')
+
+    def _get_editing_alert_message(self, entries_len):
+        return ngettext('Do you want to erase %d entry?',
+                        'Do you want to erase %d entries?',
+                         entries_len) % (entries_len)
+
+    def _get_editing_alert_operation(self):
+        return _('Erase')
+
+    def _get_info_alert_title(self):
+        return _('Erasing')
+
+    def _operate(self, metadata):
+        model.delete(metadata['uid'])
+
+        # This is sync-operation. Thus, call the callback.
+        self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchCopyButton(ToolButton, palettes.ActionItem):
+    def __init__(self):
+        ToolButton.__init__(self, 'edit-copy')
+        palettes.ActionItem.__init__(self, '', [],
+                                     show_editing_alert=True,
+                                     show_progress_info_alert=True,
+                                     batch_mode=True,
+                                     need_to_popup_options=True,
+                                     operate_on_deselected_entries=False,
+                                     switch_to_normal_mode_after_completion=True,
+                                     show_post_selected_confirmation=False,
+                                     show_not_completed_ops_info=False)
+
+        self.props.tooltip = _('Copy')
+
+        self._metadata_list = None
+
+    def _get_actionable_signal(self):
+        return 'clicked'
+
+    def _fill_and_pop_up_options(self, widget_clicked):
+        for child in self.props.palette.menu.get_children():
+            self.props.palette.menu.remove(child)
+
+        COPY_MENU_HELPER.insert_copy_to_menu_items(self.props.palette.menu,
+                                                   [],
+                                                   show_editing_alert=True,
+                                                   show_progress_info_alert=True,
+                                                   batch_mode=True)
+        self.props.palette.popup(immediate=True, state=1)
+
+
+
+
+
+
+
+
+
+class EditCopyItem(MenuItem):
+    __gtype_name__ = 'JournalEditCopyItem'
+
+    def __init__(self, icon_name, text_label, mount_path):
+        MenuItem.__init__(self, icon_name=icon_name, text_label=text_label)
+        self.mount_path = mount_path
+        self.mount_info = text_label
+
 class SortingButton(ToolButton):
     __gtype_name__ = 'JournalSortingButton'
 
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
index 417ff61..dcc3539 100644
--- a/src/jarabe/journal/listmodel.py
+++ b/src/jarabe/journal/listmodel.py
@@ -1,4 +1,8 @@
 # Copyright (C) 2009, Tomeu Vizoso
+# Copyright (C) 2012, Walter Bender  <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente  <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg      <ajay at activitycentral.com>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -54,6 +58,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
     COLUMN_BUDDY_1 = 9
     COLUMN_BUDDY_2 = 10
     COLUMN_BUDDY_3 = 11
+    COLUMN_SELECT = 12
 
     _COLUMN_TYPES = {
         COLUMN_UID: str,
@@ -68,6 +73,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
         COLUMN_BUDDY_1: object,
         COLUMN_BUDDY_3: object,
         COLUMN_BUDDY_2: object,
+        COLUMN_SELECT: bool,
     }
 
     _PAGE_SIZE = 10
@@ -78,7 +84,9 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
         self._last_requested_index = None
         self._cached_row = None
         self._result_set = model.find(query, ListModel._PAGE_SIZE)
+        self._selected = {}
         self._temp_drag_file_path = None
+        self._uid_metadata_assoc = {}
 
         # HACK: The view will tell us that it is resizing so the model can
         # avoid hitting D-Bus and disk.
@@ -93,6 +101,21 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
     def __result_set_progress_cb(self, **kwargs):
         self.emit('progress')
 
+    def update_uid_metadata_assoc(self, uid, metadata):
+        self._uid_metadata_assoc[uid] = metadata
+
+    def set_selected_value(self, uid, value):
+        if value == False:
+            del self._selected[uid]
+        elif value == True:
+            self._selected[uid] = value
+
+    def get_selected_value(self, uid):
+        if self._selected.has_key(uid):
+            return True
+        else:
+            return False
+
     def setup(self):
         self._result_set.setup()
 
@@ -102,6 +125,10 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
     def get_metadata(self, path):
         return model.get(self[path][ListModel.COLUMN_UID])
 
+    def get_in_memory_metadata(self, path):
+        uid = self[path][ListModel.COLUMN_UID]
+        return self._uid_metadata_assoc[uid]
+
     def on_get_n_columns(self):
         return len(ListModel._COLUMN_TYPES)
 
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index a0ceccc..5acbae7 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -1,4 +1,8 @@
 # Copyright (C) 2009, Tomeu Vizoso
+# Copyright (C) 2012, Walter Bender  <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente  <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg      <ajay at activitycentral.com>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -23,6 +27,7 @@ import gtk
 import hippo
 import gconf
 import pango
+import traceback
 
 from sugar.graphics import style
 from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon
@@ -98,6 +103,8 @@ class BaseListView(gtk.Bin):
         self._title_column = None
         self.sort_column = None
         self._add_columns()
+        self._inhibit_refresh = False
+        self._selected_entries = 0
 
         self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
                                                 [('text/uri-list', 0, 0),
@@ -134,6 +141,16 @@ class BaseListView(gtk.Bin):
             return object_id.startswith(self._query['mountpoints'][0])
 
     def _add_columns(self):
+        cell_select = CellRendererToggle(self.tree_view)
+        cell_select.connect('clicked', self.__cell_select_clicked_cb)
+
+        column = gtk.TreeViewColumn()
+        column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+        column.props.fixed_width = cell_select.props.width
+        column.pack_start(cell_select)
+        column.set_cell_data_func(cell_select, self.__select_set_data_cb)
+        self.tree_view.append_column(column)
+
         cell_favorite = CellRendererFavorite(self.tree_view)
         cell_favorite.connect('clicked', self.__favorite_clicked_cb)
 
@@ -242,6 +259,38 @@ class BaseListView(gtk.Bin):
         progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
         cell.props.visible = progress < 100
 
+    def __select_set_data_cb(self, column, cell, tree_model, tree_iter):
+        uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+
+        # This UI callback function may be called, when the model is
+        # still not ready.
+        # Since this function just affects the checkbox (only a UI
+        # change), so if the model is still not ready, it is safe to
+        # return at this point.
+        if uid is None:
+            return
+
+        # Hack to associate the cell with the metadata, so that it (the
+        # cell) is available offline as well (example during
+        # batch-operations, when the processing has to be done, without
+        # actually clicking any cell.
+        metadata = model.get(uid)
+        metadata['cell'] = cell
+        tree_model.update_uid_metadata_assoc(uid, metadata)
+
+        self.do_ui_select_change(metadata)
+
+    def do_ui_select_change(self, metadata):
+        tree_model = self.get_model()
+        selected = tree_model.get_selected_value(metadata['uid'])
+
+        if 'cell' in metadata.keys():
+            cell = metadata['cell']
+            if selected:
+                cell.props.icon_name = 'emblem-checked'
+            else:
+                cell.props.icon_name = 'emblem-unchecked'
+
     def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter):
         favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
         if favorite:
@@ -262,6 +311,34 @@ class BaseListView(gtk.Bin):
             metadata['keep'] = '1'
         model.write(metadata, update_mtime=False)
 
+    def __cell_select_clicked_cb(self, cell, path):
+        row = self._model[path]
+        treeiter = self._model.get_iter(path)
+        metadata = model.get(row[ListModel.COLUMN_UID])
+        self.do_backend_select_change(metadata)
+
+    def do_backend_select_change(self, metadata):
+        uid = metadata['uid']
+        selected = self._model.get_selected_value(uid)
+
+        self._model.set_selected_value(uid, not selected)
+        self._process_new_selected_status(not selected)
+
+    def _process_new_selected_status(self, new_status):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        if new_status == False:
+            self._selected_entries = self._selected_entries - 1
+            if self._selected_entries == 0:
+                journal.switch_to_editing_mode(False)
+                journal.get_list_view().inhibit_refresh(False)
+                journal.get_list_view().refresh()
+        else:
+            self._selected_entries = self._selected_entries + 1
+            journal.get_list_view().inhibit_refresh(True)
+            journal.switch_to_editing_mode(True)
+
     def update_with_query(self, query_dict):
         logging.debug('ListView.update_with_query')
         if 'order_by' not in query_dict:
@@ -274,9 +351,14 @@ class BaseListView(gtk.Bin):
                              ListModel.COLUMN_TIMESTAMP))
         self._query = query_dict
 
+        # This refresh is always needed, since the query has changed.
         self.refresh()
 
     def refresh(self):
+        if not self._inhibit_refresh:
+            self.proceed_with_refresh()
+
+    def proceed_with_refresh(self):
         logging.debug('ListView.refresh query %r', self._query)
         self._stop_progress_bar()
 
@@ -466,6 +548,12 @@ class BaseListView(gtk.Bin):
         self.update_dates()
         return True
 
+    def get_model(self):
+        return self._model
+
+    def inhibit_refresh(self, inhibit):
+        self._inhibit_refresh = inhibit
+
 
 class ListView(BaseListView):
     __gtype_name__ = 'JournalListView'
@@ -550,6 +638,17 @@ class ListView(BaseListView):
     def __editing_canceled_cb(self, cell):
         self.cell_title.props.editable = False
 
+class CellRendererToggle(CellRendererIcon):
+    __gtype_name__ = 'JournalCellRendererSelect'
+
+    def __init__(self, tree_view):
+        CellRendererIcon.__init__(self, tree_view)
+
+        self.props.width = style.GRID_CELL_SIZE
+        self.props.height = style.GRID_CELL_SIZE
+        self.props.size = style.SMALL_ICON_SIZE
+        self.props.icon_name = 'checkbox-unchecked'
+        self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
 
 class CellRendererFavorite(CellRendererIcon):
     __gtype_name__ = 'JournalCellRendererFavorite'
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 5285a7c..83e216f 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -1,4 +1,8 @@
 # Copyright (C) 2007-2011, One Laptop per Child
+# Copyright (C) 2012, Walter Bender  <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente  <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg      <ajay at activitycentral.com>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -37,7 +41,6 @@ from sugar import dispatch
 from sugar import mime
 from sugar import util
 
-
 DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
 DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
 DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
@@ -45,7 +48,8 @@ DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
 # Properties the journal cares about.
 PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id',
               'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type',
-              'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid']
+              'mountpoint', 'mtime', 'progress', 'timestamp', 'title',
+              'uid']
 
 MIN_PAGES_TO_CACHE = 3
 MAX_PAGES_TO_CACHE = 5
@@ -651,6 +655,11 @@ def write(metadata, file_path='', update_mtime=True, transfer_ownership=True):
                                                  file_path,
                                                  transfer_ownership)
     else:
+        # HACK: For documents: modify the mount-point
+        from jarabe.journal.journalactivity import get_mount_point
+        if get_mount_point() == get_documents_path():
+            metadata['mountpoint'] = get_documents_path()
+
         object_id = _write_entry_on_external_device(metadata, file_path)
 
     return object_id
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 27b0b54..56275e7 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -1,4 +1,8 @@
 # Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2012, Walter Bender  <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente  <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg      <ajay at activitycentral.com>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -15,6 +19,7 @@
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
 from gettext import gettext as _
+from gettext import ngettext
 import logging
 import os
 
@@ -23,6 +28,9 @@ import gtk
 import gconf
 import gio
 import glib
+import time
+
+from sugar import _sugarext
 
 from sugar.graphics import style
 from sugar.graphics.palette import Palette
@@ -39,6 +47,8 @@ from jarabe.journal import model
 
 friends_model = friends.get_model()
 
+_copy_menu_helper = None
+
 
 class BulkOperationDetails():
 
@@ -129,7 +139,16 @@ class ObjectPalette(Palette):
         menu_item.set_image(icon)
         self.menu.append(menu_item)
         menu_item.show()
-        copy_menu = CopyMenu(metadata)
+        copy_menu = CopyMenu()
+        copy_menu_helper = get_copy_menu_helper()
+
+        metadata_list = []
+        metadata_list.append(metadata)
+        copy_menu_helper.insert_copy_to_menu_items(copy_menu,
+                                                   metadata_list,
+                                                   False,
+                                                   False,
+                                                   False)
         copy_menu.connect('volume-error', self.__volume_error_cb)
         menu_item.set_submenu(copy_menu)
 
@@ -260,156 +279,512 @@ class CopyMenu(gtk.Menu):
                          ([str, str])),
     }
 
-    def __init__(self, metadata):
+    def __init__(self):
         gobject.GObject.__init__(self)
 
-        self._metadata = metadata
 
-        clipboard_menu = ClipboardMenu(self._metadata)
-        clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
-                                      icon_size=gtk.ICON_SIZE_MENU))
-        clipboard_menu.connect('volume-error', self.__volume_error_cb)
-        self.append(clipboard_menu)
-        clipboard_menu.show()
+class ActionItem(gobject.GObject):
+    """
+    This class implements the course of actions that happens when clicking
+    upon an Action-Item (eg. Batch-Copy-Toolbar-button;
+                             Actual-Batch-Copy-To-Journal-button;
+                             Actual-Batch-Copy-To-Documents-button;
+                             Actual-Batch-Copy-To-Mounted-Drive-button;
+                             Actual-Batch-Copy-To-Clipboard-button;
+                             Single-Copy-To-Journal-button;
+                             Single-Copy-To-Documents-button;
+                             Single-Copy-To-Mounted-Drive-button;
+                             Single-Copy-To-Clipboard-button;
+                             Batch-Erase-Button;
+                             Select-None-Toolbar-button;
+                             Select-All-Toolbar-button
+    """
 
-        from jarabe.journal import journalactivity
-        journal_model = journalactivity.get_journal()
-        if journal_model.get_mount_point() != model.get_documents_path():
-            documents_menu = DocumentsMenu(self._metadata)
-            documents_menu.set_image(Icon(icon_name='user-documents',
-                                          icon_size=gtk.ICON_SIZE_MENU))
-            documents_menu.connect('volume-error', self.__volume_error_cb)
-            self.append(documents_menu)
-            documents_menu.show()
+    def __init__(self, label, metadata_list, show_editing_alert,
+                 show_progress_info_alert, batch_mode,
+                 need_to_popup_options,
+                 operate_on_deselected_entries,
+                 switch_to_normal_mode_after_completion,
+                 show_post_selected_confirmation,
+                 show_not_completed_ops_info):
+        gobject.GObject.__init__(self)
 
-        if self._metadata['mountpoint'] != '/':
-            client = gconf.client_get_default()
-            color = XoColor(client.get_string('/desktop/sugar/user/color'))
-            journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
-            journal_menu.set_image(Icon(icon_name='activity-journal',
-                                        xo_color=color,
-                                        icon_size=gtk.ICON_SIZE_MENU))
-            journal_menu.connect('volume-error', self.__volume_error_cb)
-            self.append(journal_menu)
-            journal_menu.show()
+        self._label = label
+
+        # Make a copy.
+        self._immutable_metadata_list = []
+        for metadata in metadata_list:
+            self._immutable_metadata_list.append(metadata)
+
+        self._metadata_list = metadata_list
+        self._show_progress_info_alert = show_progress_info_alert
+        self._batch_mode = batch_mode
+        self._operate_on_deselected_entries = \
+                operate_on_deselected_entries
+        self._switch_to_normal_mode_after_completion = \
+                switch_to_normal_mode_after_completion
+        self._show_post_selected_confirmation = \
+                show_post_selected_confirmation
+        self._show_not_completed_ops_info = \
+                show_not_completed_ops_info
+
+        actionable_signal = self._get_actionable_signal()
+
+        if need_to_popup_options:
+            self.connect(actionable_signal, self._fill_and_pop_up_options)
+        else:
+            if show_editing_alert:
+                self.connect(actionable_signal, self._show_editing_alert)
+            else:
+                self.connect(actionable_signal, self._pre_operate_per_action)
+
+    def _get_actionable_signal(self):
+        """
+        Some widgets like 'buttons' have 'clicked' as actionable signal;
+        some like 'menuitems' have 'activate' as actionable signal.
+        """
+
+        raise NotImplementedError
+
+    def _fill_and_pop_up_options(self):
+        """
+        Eg. Batch-Copy-Toolbar-button does not do anything by itself
+        useful; but rather pops-up the actual 'copy-to' options.
+        """
+
+        raise NotImplementedError
+
+    def _show_editing_alert(self, widget_clicked):
+        """
+        Upon clicking the actual operation button (eg.
+        Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT
+        Batch-Copy-Toolbar-button, since it does not do anything
+        actually useful, but only pops-up the actual 'copy-to' options.
+        """
+
+        alert_parameters = self._get_editing_alert_parameters()
+        title = alert_parameters[0]
+        message = alert_parameters[1]
+        operation = alert_parameters[2]
+
+        from jarabe.journal.journalactivity import get_journal
+        get_journal().update_confirmation_alert(title, message,
+                                                self._pre_operate_per_action,
+                                                None)
+
+    def _get_editing_alert_parameters(self):
+        """
+        Get the alert parameters for widgets that can show editing
+        alert.
+        """
+
+        self._metadata_list = self._get_metadata_list()
+        entries_len = len(self._metadata_list)
+
+        title = self._get_editing_alert_title()
+        message = self._get_editing_alert_message(entries_len)
+        operation = self._get_editing_alert_operation()
+
+        return (title, message, operation)
+
+    def _get_list_model_len(self):
+        """
+        Get the total length of the model under view.
+        """
+
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        return len(journal.get_list_view().get_model())
+
+    def _get_metadata_list(self):
+        """
+        For batch-mode, get the metadata list, according to button-type.
+        For eg, Select-All-Toolbar-button operates on non-selected entries;
+        while othere operate on selected-entries.
+
+        For single-mode, simply copy from the
+        "immutable_metadata_list".
+        """
+
+        if self._batch_mode:
+            from jarabe.journal.journalactivity import get_journal
+            journal = get_journal()
+
+            if self._operate_on_deselected_entries:
+                return journal.get_metadata_list(False)
+            else:
+                return journal.get_metadata_list(True)
+        else:
+            metadata_list = []
+            for metadata in self._immutable_metadata_list:
+                metadata_list.append(metadata)
+            return metadata_list
 
-        volume_monitor = gio.volume_monitor_get()
-        icon_theme = gtk.icon_theme_get_default()
-        for mount in volume_monitor.get_mounts():
-            if self._metadata['mountpoint'] == mount.get_root().get_path():
-                continue
-            volume_menu = VolumeMenu(self._metadata, mount.get_name(),
-                                   mount.get_root().get_path())
-            for name in mount.get_icon().props.names:
-                if icon_theme.has_icon(name):
-                    volume_menu.set_image(Icon(icon_name=name,
-                                               icon_size=gtk.ICON_SIZE_MENU))
-                    break
-            volume_menu.connect('volume-error', self.__volume_error_cb)
-            self.append(volume_menu)
-            volume_menu.show()
+    def _get_editing_alert_title(self):
+        raise NotImplementedError
 
-    def __volume_error_cb(self, menu_item, message, severity):
-        self.emit('volume-error', message, severity)
+    def _get_editing_alert_message(self, entries_len):
+        raise NotImplementedError
 
+    def _get_editing_alert_operation(self):
+        raise NotImplementedError
 
-class VolumeMenu(MenuItem):
-    __gtype_name__ = 'JournalVolumeMenu'
+    def _is_metadata_list_empty(self):
+        return (self._metadata_list is None) or \
+                (len(self._metadata_list) == 0)
 
-    __gsignals__ = {
-        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
-                         ([str, str])),
-    }
+    def _pre_operate_per_action(self, obj, ok_clicked=False):
+        """
+        This is the stage, just before the FIRST metadata gets into its
+        processing cycle.
+        """
 
-    def __init__(self, metadata, label, mount_point):
-        MenuItem.__init__(self, label)
-        self._metadata = metadata
-        self.connect('activate', self.__copy_to_volume_cb, mount_point)
+        # Show waiting cursor (only for batch mode)
+        if self._batch_mode:
+            self.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
 
-    def __copy_to_volume_cb(self, menu_item, mount_point):
-        file_path = model.get_file(self._metadata['uid'])
+        self._skip_all = False
+
+        # Also, get the initial length of the model.
+        self._model_len = self._get_list_model_len()
+
+        # For batch-operations, fetch the metadata list again.
+        self._metadata_list = self._get_metadata_list()
+
+        # Set the initial length of metadata-list.
+        self._metadata_list_initial_len = len(self._metadata_list)
+
+        self._metadata_processed = 0
+
+        # Next, proceed with the metadata
+        self._pre_operate_per_metadata_per_action()
+
+    def _pre_operate_per_metadata_per_action(self):
+        """
+        This is the stage, just before EVERY metadata gets into doing
+        its actual work.
+        """
+
+        from jarabe.journal.journalactivity import get_journal
+
+        # If there is still some metadata left, proceed with the
+        # metadata operation.
+        # Else, proceed to post-operations.
+        if len(self._metadata_list) > 0:
+            metadata = self._metadata_list.pop(0)
+
+            # If info-alert needs to be shown, show the alert, and
+            # arrange for actual operation.
+            # Else, proceed to actual operation directly.
+            if self._show_progress_info_alert:
+                current_len = len(self._metadata_list)
+
+                # TRANS: Do not translate the two %d, and the %s.
+                info_alert_message = _('( %d / %d ) %s') % (
+                        self._metadata_list_initial_len - current_len,
+                        self._metadata_list_initial_len, metadata['title'])
 
+                get_journal().update_info_alert(self._get_info_alert_title() + ' ...',
+                                                info_alert_message,
+                                                self._operate_per_metadata_per_action,
+                                                metadata)
+            else:
+                self._operate_per_metadata_per_action(metadata)
+        else:
+            self._post_operate_per_action()
+
+    def _get_info_alert_title(self):
+        raise NotImplementedError
+
+    def _operate_per_metadata_per_action(self, metadata):
+        """
+        This is just a code-convenient-function, which allows
+        runtime-overriding. It just delegates to the actual
+        "self._operate" method, the actual which is determined at
+        runtime.
+        """
+
+        # Pass the callback for the post-operation-for-metadata. This
+        # will ensure that async-operations on the metadata are taken
+        # care of.
+        if self._operate(metadata) is False:
+            return
+        else:
+            self._metadata_processed = self._metadata_processed + 1
+
+
+    def _operate(self, metadata):
+        """
+        Actual, core, productive stage for EVERY metadata.
+        """
+
+        raise NotImplementedError
+
+    def  _post_operate_per_metadata_per_action(self, metadata):
+        """
+        This is the stage, just after EVERY metadata has been
+        processed.
+        """
+
+        # Toggle the corresponding checkbox - but only for batch-mode.
+        if self._batch_mode:
+            from jarabe.journal.journalactivity import get_journal
+            list_view = get_journal().get_list_view()
+
+            list_view.do_ui_select_change(metadata)
+            list_view.do_backend_select_change(metadata)
+
+        # Call the next ...
+        self._pre_operate_per_metadata_per_action()
+
+    def _post_operate_per_action(self):
+        """
+        This is the stage, just after the LAST metadata has been
+        processed.
+        """
+
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        # Show post-operation confirmation message, if applicable.
+        if self._show_post_selected_confirmation:
+            entries_len = \
+                    self._get_post_selection_alert_message_entries_len()
+            message = \
+                    self._get_post_selection_alert_message(entries_len)
+            journal.update_error_alert(self._get_editing_alert_operation(),
+                                      message,
+                                      self._process_switching_mode,
+                                      None)
+        else:
+            self._process_switching_mode(None, False)
+
+        # Retain the old cursor.
+        if self._batch_mode:
+            self.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
+
+    def _process_switching_mode(self, metadata, ok_clicked=False):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        # Necessary to do this, when the alert needs to be hidden,
+        # WITHOUT user-intervention.
+        journal.hide_alert()
+
+    def _refresh(self):
+        from jarabe.journal.journalactivity import get_journal
+        get_journal().get_list_view().refresh()
+
+    def _handle_error_alert(self, error_message, metadata):
+        """
+        This handles any error scenarios. Examples are of entries that
+        display the message "Entries without a file cannot be copied."
+        This is kind of controller-functionl the model-function is
+        "self._set_error_info_alert".
+        """
+
+        if self._skip_all:
+            self._post_operate_per_metadata_per_action(metadata)
+        else:
+            self._set_error_info_alert(error_message, metadata)
+
+    def _set_error_info_alert(self, error_message, metadata):
+        """
+        This method displays the error alert.
+        """
+
+        current_len = len(self._metadata_list)
+
+        # TRANS: Do not translate the two %d, and the three %s.
+        info_alert_message = _('( %d / %d ) Error while %s %s : %s') % (
+                self._metadata_list_initial_len - current_len,
+                self._metadata_list_initial_len,
+                self._get_info_alert_title(),
+                metadata['title'],
+                error_message)
+
+        # Only show the alert, if allowed to.
+        if self._show_not_completed_ops_info:
+            from jarabe.journal.journalactivity import get_journal
+            get_journal().update_error_alert(self._get_info_alert_title()  + ' ...',
+                                             info_alert_message,
+                                             self._process_error_skipping,
+                                             metadata)
+        else:
+            self._process_error_skipping(self._skip_all, metadata)
+
+    def _process_error_skipping(self, metadata, skip_all):
+        # The operation for the current metadata is finished (kinda
+        # pseudo ...)
+        self._post_operate_per_metadata_per_action(metadata)
+
+    def _file_path_valid(self, metadata):
+        file_path = model.get_file(metadata['uid'])
         if not file_path or not os.path.exists(file_path):
             logging.warn('Entries without a file cannot be copied.')
-            self.emit('volume-error',
-                      _('Entries without a file cannot be copied.'),
-                      _('Warning'))
-            return
+            error_message =  _('Entries without a file cannot be copied.')
+            if self._batch_mode:
+                self._handle_error_alert(error_message, metadata)
+            else:
+                self.emit('volume-error', error_message, _('Warning'))
+            return False
+        else:
+            return True
+
+    def _metadata_copy_valid(self, metadata, mount_point):
+        self._set_bundle_installation_allowed(False)
 
         try:
-            model.copy(self._metadata, mount_point)
-        except IOError, e:
-            logging.exception('Error while copying the entry. %s', e.strerror)
-            self.emit('volume-error',
-                      _('Error while copying the entry. %s') % e.strerror,
-                      _('Error'))
+            model.copy(metadata, mount_point)
+            return True
+        except Exception, e:
+            logging.exception('Error while copying the entry. %s', e)
+            error_message = _('Error while copying the entry. %s') % e
+            if self._batch_mode:
+                self._handle_error_alert(error_message, metadata)
+            else:
+                self.emit('volume-error', error_message, _('Error'))
+            return False
+        finally:
+            self._set_bundle_installation_allowed(True)
 
+    def _metadata_write_valid(self, metadata):
+        operation = self._get_info_alert_title()
+        self._set_bundle_installation_allowed(False)
 
-class ClipboardMenu(MenuItem):
-    __gtype_name__ = 'JournalClipboardMenu'
+        try:
+            model.write(metadata, update_mtime=False)
+            return True
+        except Exception, e:
+            logging.exception('Error while writing the metadata. %s', e)
+            error_message = _('Error occurred while %s : %s.') % \
+                    (operation, e,)
+            if self._batch_mode:
+                self._handle_error_alert(error_message, metadata)
+            else:
+                self.emit('volume-error', error_message, _('Error'))
+            return False
+        finally:
+            self._set_bundle_installation_allowed(True)
 
-    __gsignals__ = {
-        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
-                         ([str, str])),
-    }
+    def _set_bundle_installation_allowed(self, allowed):
+        """
+        This method serves only as a "delegating" method.
+        This has been done to aid easy configurability.
+        """
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
 
-    def __init__(self, metadata):
-        MenuItem.__init__(self, _('Clipboard'))
+        if self._batch_mode:
+            journal.set_bundle_installation_allowed(allowed)
 
-        self._temp_file_path = None
-        self._metadata = metadata
-        self.connect('activate', self.__copy_to_clipboard_cb)
 
-    def __copy_to_clipboard_cb(self, menu_item):
-        file_path = model.get_file(self._metadata['uid'])
-        if not file_path or not os.path.exists(file_path):
-            logging.warn('Entries without a file cannot be copied.')
-            self.emit('volume-error',
-                      _('Entries without a file cannot be copied.'),
-                      _('Warning'))
-            return
+class BaseCopyMenuItem(MenuItem, ActionItem):
+    __gsignals__ = {
+            'volume-error': (gobject.SIGNAL_RUN_FIRST,
+                             gobject.TYPE_NONE, ([str, str])),
+            }
+
+    def __init__(self, metadata_list, label, show_editing_alert,
+                 show_progress_info_alert, batch_mode):
+        MenuItem.__init__(self, label)
+        ActionItem.__init__(self, label, metadata_list, show_editing_alert,
+                            show_progress_info_alert, batch_mode,
+                            need_to_popup_options=False,
+                            operate_on_deselected_entries=False,
+                            switch_to_normal_mode_after_completion=True,
+                            show_post_selected_confirmation=False,
+                            show_not_completed_ops_info=True)
+
+    def _get_actionable_signal(self):
+        return 'activate'
+
+    def _get_editing_alert_title(self):
+        return _('Copy')
+
+    def _get_editing_alert_message(self, entries_len):
+        return ngettext('Do you want to copy %d entry to %s?',
+                        'Do you want to copy %d entries to %s?',
+                        entries_len) % (entries_len, self._label)
+
+    def _get_editing_alert_operation(self):
+        return _('Copy')
+
+    def _get_info_alert_title(self):
+        return _('Copying')
+
+
+class VolumeMenu(BaseCopyMenuItem):
+    def __init__(self, metadata_list, label, mount_point,
+                 show_editing_alert, show_progress_info_alert,
+                 batch_mode):
+        BaseCopyMenuItem.__init__(self, metadata_list, label,
+                                  show_editing_alert,
+                                  show_progress_info_alert, batch_mode)
+        self._mount_point = mount_point
+
+    def _operate(self, metadata):
+        if not self._file_path_valid(metadata):
+            return False
+        if not self._metadata_copy_valid(metadata, self._mount_point):
+            return False
+
+        # This is sync-operation. Thus, call the callback.
+        self._post_operate_per_metadata_per_action(metadata)
+
+
+class ClipboardMenu(BaseCopyMenuItem):
+    def __init__(self, metadata_list, show_editing_alert,
+                 show_progress_info_alert, batch_mode):
+        BaseCopyMenuItem.__init__(self, metadata_list, _('Clipboard'),
+                                  show_editing_alert,
+                                  show_progress_info_alert,
+                                  batch_mode)
+        self._temp_file_path_list = []
+
+    def _operate(self, metadata):
+        if not self._file_path_valid(metadata):
+            return False
 
         clipboard = gtk.Clipboard()
         clipboard.set_with_data([('text/uri-list', 0, 0)],
                                 self.__clipboard_get_func_cb,
-                                self.__clipboard_clear_func_cb)
+                                self.__clipboard_clear_func_cb,
+                                metadata)
 
-    def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+    def __clipboard_get_func_cb(self, clipboard, selection_data, info,
+                                metadata):
         # Get hold of a reference so the temp file doesn't get deleted
-        self._temp_file_path = model.get_file(self._metadata['uid'])
+        self._temp_file_path = model.get_file(metadata['uid'])
         logging.debug('__clipboard_get_func_cb %r', self._temp_file_path)
         selection_data.set_uris(['file://' + self._temp_file_path])
 
-    def __clipboard_clear_func_cb(self, clipboard, data):
+    def __clipboard_clear_func_cb(self, clipboard, metadata):
         # Release and delete the temp file
         self._temp_file_path = None
 
+        # This is async-operation; and this is the ending point.
+        self._post_operate_per_metadata_per_action(metadata)
 
-class DocumentsMenu(MenuItem):
-    __gtype_name__ = 'JournalDocumentsMenu'
 
-    __gsignals__ = {
-        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
-                         ([str, str])),
-    }
+class DocumentsMenu(BaseCopyMenuItem):
+    def __init__(self, metadata_list, show_editing_alert,
+                 show_progress_info_alert, batch_mode):
+        BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'),
+                                  show_editing_alert,
+                                  show_progress_info_alert,
+                                  batch_mode)
 
-    def __init__(self, metadata):
-        MenuItem.__init__(self, _('Documents'))
+    def _operate(self, metadata):
+        if not self._file_path_valid(metadata):
+            return False
+        if not self._metadata_copy_valid(metadata,
+                                         model.get_documents_path()):
+            return False
 
-        self._temp_file_path = None
-        self._metadata = metadata
-        self.connect('activate', self.__copy_to_documents_cb)
-
-    def __copy_to_documents_cb(self, menu_item):
-        file_path = model.get_file(self._metadata['uid'])
-        if not file_path or not os.path.exists(file_path):
-            logging.warn('Entries without a file cannot be copied.')
-            self.emit('volume-error',
-                      _('Entries without a file cannot be copied.'),
-                      _('Warning'))
-            return
-
-        model.copy(self._metadata, model.get_documents_path())
+        # This is sync-operation. Call the post-operation now.
+        self._post_operate_per_metadata_per_action(metadata)
 
 
 class GroupsMenu(gtk.Menu):
@@ -538,3 +913,90 @@ class BuddyPalette(Palette):
                          icon=buddy_icon)
 
         # TODO: Support actions on buddies, like make friend, invite, etc.
+
+
+
+class CopyMenuHelper(gtk.Menu):
+    __gtype_name__ = 'JournalCopyMenuHelper'
+
+    __gsignals__ = {
+            'volume-error': (gobject.SIGNAL_RUN_FIRST,
+                             gobject.TYPE_NONE,
+                             ([str, str])),
+            }
+
+    def insert_copy_to_menu_items(self, menu, metadata_list,
+                                  show_editing_alert,
+                                  show_progress_info_alert,
+                                  batch_mode):
+        self._metadata_list = metadata_list
+
+        clipboard_menu = ClipboardMenu(metadata_list,
+                                       show_editing_alert,
+                                       show_progress_info_alert,
+                                       batch_mode)
+        clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
+                                      icon_size=gtk.ICON_SIZE_MENU))
+        clipboard_menu.connect('volume-error', self.__volume_error_cb)
+        menu.append(clipboard_menu)
+        clipboard_menu.show()
+
+        from jarabe.journal.journalactivity import get_mount_point
+
+        if get_mount_point() != model.get_documents_path():
+            documents_menu = DocumentsMenu(metadata_list,
+                                           show_editing_alert,
+                                           show_progress_info_alert,
+                                           batch_mode)
+            documents_menu.set_image(Icon(icon_name='user-documents',
+                                          icon_size=gtk.ICON_SIZE_MENU))
+            documents_menu.connect('volume-error', self.__volume_error_cb)
+            menu.append(documents_menu)
+            documents_menu.show()
+
+        if get_mount_point() != '/':
+            client = gconf.client_get_default()
+            color = XoColor(client.get_string('/desktop/sugar/user/color'))
+            journal_menu = VolumeMenu(metadata_list, _('Journal'), '/',
+                                      show_editing_alert,
+                                      show_progress_info_alert,
+                                      batch_mode)
+            journal_menu.set_image(Icon(icon_name='activity-journal',
+                                        xo_color=color,
+                                        icon_size=gtk.ICON_SIZE_MENU))
+            journal_menu.connect('volume-error', self.__volume_error_cb)
+            menu.append(journal_menu)
+            journal_menu.show()
+
+        volume_monitor = gio.volume_monitor_get()
+        icon_theme = gtk.icon_theme_get_default()
+        for mount in volume_monitor.get_mounts():
+            if get_mount_point() == mount.get_root().get_path():
+                continue
+
+            volume_menu = VolumeMenu(metadata_list, mount.get_name(),
+                                     mount.get_root().get_path(),
+                                     show_editing_alert,
+                                     show_progress_info_alert,
+                                     batch_mode)
+            for name in mount.get_icon().props.names:
+                if icon_theme.has_icon(name):
+                    volume_menu.set_image(Icon(icon_name=name,
+                                               icon_size=gtk.ICON_SIZE_MENU))
+                    break
+
+            volume_menu.connect('volume-error', self.__volume_error_cb)
+            menu.insert(volume_menu, -1)
+            volume_menu.show()
+
+    def __volume_error_cb(self, menu_item, message, severity):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+        journal._volume_error_cb(menu_item, message, severity)
+
+
+def get_copy_menu_helper():
+    global _copy_menu_helper
+    if _copy_menu_helper is None:
+        _copy_menu_helper = CopyMenuHelper()
+    return _copy_menu_helper
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 1356099..c591cc4 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -297,6 +297,15 @@ class VolumesToolbar(gtk.Toolbar):
         button = self._get_button_for_mount(mount)
         button.props.active = True
 
+    def set_volume_buttons_sensitive(self, sensitive, mount_point):
+        """
+        Toggles the state of all volume-buttons, except the currently
+        active mount-point.
+        """
+        for button in self._volume_buttons:
+            if button.mount_point != mount_point:
+                button.set_sensitive(sensitive)
+
 
 class BaseButton(RadioToolButton):
     __gsignals__ = {
-- 
1.7.4.4



More information about the Sugar-devel mailing list