[Sugar-devel] [sugar PATCH] Multi-Select feature.

Ajay Garg ajay at activitycentral.com
Fri Aug 17 13:43:48 EDT 2012


This patch adds the "Multi-Select" facility, to easy copying/erasing multiple
items in one go (after selecting the required "target" entries).

Note that, the corresponding "sugar-toolkit" and "sugar-artwork" patches also need to be applied,
for this feature to work.




====================
Courtesy Gary Martin, this feature has been made fully robust and bullet-proof.
====================

In particular, following things are intended via this patch ::

a)
Solves the basic purpose ( of course :P )


b)
There should be no sequence of events, that renders the UI in unusable state.


c)
There should be no moment, wherein the user may act "impatient", and may 
cause an undesirable sequence of actions (may/may-not be leading to
an unusable state).


d)
Speed optimisation, as far, and as logically, as possible.


Again, all credit goes to Gary, for having rendered this feature such robustness !!!






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






The only "issue" that may be hit while testing this feature, is the bug ::
http://bugs.sugarlabs.org/ticket/3813

which actually, has nothing to do with this feature as per say; it occurs even with
this patch unapplied.


 src/jarabe/journal/journalactivity.py |  151 +++++++-
 src/jarabe/journal/journaltoolbox.py  |  297 ++++++++++++--
 src/jarabe/journal/journalwindow.py   |   44 ++
 src/jarabe/journal/listmodel.py       |   23 +
 src/jarabe/journal/listview.py        |  211 ++++++++++-
 src/jarabe/journal/model.py           |   45 ++-
 src/jarabe/journal/palettes.py        |  733 +++++++++++++++++++++++++++++----
 src/jarabe/journal/volumestoolbar.py  |   50 ++-
 8 files changed, 1415 insertions(+), 139 deletions(-)

diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index bb1c7f6..7ed8b18 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -17,15 +17,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, NButtonAlert
+from sugar.graphics.icon import Icon
 
 from sugar.bundle.bundle import ZipExtractException, RegistrationException
 from sugar import env
@@ -34,7 +37,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
@@ -43,6 +48,7 @@ from jarabe.journal.objectchooser import ObjectChooser
 from jarabe.journal.modalalert import ModalAlert
 from jarabe.journal import model
 from jarabe.journal.journalwindow import JournalWindow
+from jarabe.journal.journalwindow import show_normal_cursor
 
 
 J_DBUS_SERVICE = 'org.laptop.Journal'
@@ -53,6 +59,7 @@ _SPACE_TRESHOLD = 52428800
 _BUNDLE_ID = 'org.laptop.JournalActivity'
 
 _journal = None
+_mount_point = None
 
 
 class JournalActivityDBusService(dbus.service.Object):
@@ -119,8 +126,31 @@ 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 = NButtonAlert()
+        self._error_alert._populate_buttons([
+            ('dialog-ok', gtk.RESPONSE_OK, _('Ok'))
+                                            ])
+
+        self._confirmation_alert = NButtonAlert()
+        self._confirmation_alert._populate_buttons([
+            ('dialog-cancel', gtk.RESPONSE_CANCEL, _('Stop')),
+            ('dialog-ok', gtk.RESPONSE_OK, _('Continue'))
+                                                   ])
+
+        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 +181,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,6 +217,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('/')
+        set_mount_point('/')
 
     def _setup_secondary_view(self):
         self._secondary_view = gtk.VBox()
@@ -216,9 +250,14 @@ 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:
+            self._toolbox = EditToolbox()
+            self._toolbox.set_total_number_of_entries(self.get_total_number_of_entries())
+        else:
+            self._toolbox = self._main_toolbox
+
+        self.set_toolbar_box(self._toolbox)
+        self._toolbox.show()
 
         if self.canvas != self._main_view:
             self.set_canvas(self._main_view)
@@ -253,6 +292,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)
+        set_mount_point(mount_point)
         self._main_toolbox.set_current_toolbar(0)
 
     def __model_created_cb(self, sender, **kwargs):
@@ -279,6 +319,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)
@@ -314,6 +357,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()
@@ -362,6 +408,95 @@ class JournalActivity(JournalWindow):
         self.show_main_view()
         self.search_grab_focus()
 
+    def switch_to_editing_mode(self, switch):
+        # (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:
+            gobject.idle_add(self._callback, self._data,
+                             response_id)
+
+    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()
+        show_normal_cursor()
+
+    def hide_alert(self):
+        if self._current_alert is not None:
+            self._current_alert.hide()
+
+    def update_info_alert(self, title, message):
+        self.get_toolbar_box().display_running_status_in_multi_select(title, message)
+
+    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_total_number_of_entries(self):
+        list_view_model = self.get_list_view().get_model()
+        return len(list_view_model)
+
+    def is_editing_mode_present(self):
+        return self._editing_mode
+
+    def get_volumes_toolbar(self):
+        return self._volumes_toolbar
+
+    def get_toolbar_box(self):
+        return self._toolbox
+
 
 def get_journal():
     global _journal
@@ -373,3 +508,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..96b940b 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -16,6 +16,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 +44,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 +457,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 +501,259 @@ 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()
+
+    def process_new_selected_entry_in_multi_select(self):
+        self.edit_toolbar._update_info('', '', True, True)
+
+    def process_new_deselected_entry_in_multi_select(self):
+        self.edit_toolbar._update_info('', '', False, True)
+
+    def display_running_status_in_multi_select(self, primary_info,
+                                               secondary_info):
+        self.edit_toolbar._update_info(primary_info, secondary_info, None, True)
+
+    def display_already_selected_entries_status(self):
+        self.edit_toolbar._update_info('', '', True, False)
+
+    def set_total_number_of_entries(self, total):
+        self.edit_toolbar._set_total_number_of_entries(total)
+
+    def get_current_entry_number(self):
+        return self.edit_toolbar._get_current_entry_number()
+
+
+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.add(gtk.SeparatorToolItem())
+
+        self._multi_select_info_widget = MultiSelectEntriesInfoWidget()
+        self.add(self._multi_select_info_widget)
+
+        self.show_all()
+
+    def _set_total_number_of_entries(self, total):
+        self._multi_select_info_widget.set_total_number_of_entries(total)
+
+    def _update_info(self, primary_info, secondary_info,
+                     special_action, update_selected_entries):
+        gobject.idle_add(self._multi_select_info_widget.update_text,
+                         primary_info, secondary_info,
+                         special_action, update_selected_entries)
+
+    def _get_current_entry_number(self):
+        return self._multi_select_info_widget.get_current_entry_number()
+
+
+class SelectNoneButton(ToolButton):
+    def __init__(self):
+        ToolButton.__init__(self, 'select-none')
+        self.props.tooltip = _('Deselect all')
+
+        self.connect('clicked', self.__do_deselect_all)
+
+    def __do_deselect_all(self, widget_clicked):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        journal.get_list_view()._selected_entries = 0
+        journal.switch_to_editing_mode(False)
+        journal.get_list_view().inhibit_refresh(False)
+        journal.get_list_view().refresh()
+
+
+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=False,
+                                     batch_mode=True,
+                                     auto_deselect_source_entries=True,
+                                     need_to_popup_options=False,
+                                     operate_on_deselected_entries=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,
+                                     auto_deselect_source_entries=True,
+                                     need_to_popup_options=False,
+                                     operate_on_deselected_entries=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,
+                                     auto_deselect_source_entries=False,
+                                     need_to_popup_options=True,
+                                     operate_on_deselected_entries=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 MultiSelectEntriesInfoWidget(gtk.ToolItem):
+    def __init__(self):
+        gtk.ToolItem.__init__(self)
+
+        self._selected_entries = 0
+
+        self._label = gtk.Label()
+        self.add(self._label)
+
+        self.show_all()
+
+    def set_total_number_of_entries(self, total):
+        self._total = total
+
+    def update_text(self, primary_text, secondary_text, special_action,
+                    update_selected_entries):
+        # If "special_action" is None,
+        #       we need to display the info, conveyed by
+        #       "primary_message" and "secondary_message"
+        #
+        # If "special_action" is True,
+        #       a new entry has been selected.
+        #
+        # If "special_action" is False,
+        #       an enrty has been deselected.
+        if special_action == None:
+            self._label.set_text(primary_text + secondary_text)
+            self._label.show()
+        else:
+            if update_selected_entries:
+                if special_action == True:
+                    self._selected_entries = self._selected_entries + 1
+                elif special_action == False:
+                    self._selected_entries = self._selected_entries - 1
+
+            # TRANS: Do not translate the two "%d".
+            message = _('Selected %d of %d') % (self._selected_entries,
+                                                self._total)
+
+            # Only show the "selected x of y" for "Select All", or
+            # "Deselect All", or if the user checked/unchecked a
+            # checkbox.
+            from jarabe.journal.palettes import get_current_action_item
+            current_action_item = get_current_action_item()
+            if current_action_item == None or \
+               isinstance(current_action_item, SelectAllButton) or \
+               isinstance(current_action_item, SelectNoneButton):
+                   self._label.set_text(message)
+                   self._label.show()
+
+    def get_current_entry_number(self):
+        return self._selected_entries
+
+
+
+
+
+
+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/journalwindow.py b/src/jarabe/journal/journalwindow.py
index 31bc790..25ac478 100644
--- a/src/jarabe/journal/journalwindow.py
+++ b/src/jarabe/journal/journalwindow.py
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+import gtk
 
 from sugar.graphics.window import Window
 
@@ -31,3 +32,46 @@ class JournalWindow(Window):
 
 def get_journal_window():
     return _journal_window
+
+
+def set_widgets_active_state(active_state):
+    from jarabe.journal.journalactivity import get_journal
+    journal = get_journal()
+
+    journal.get_toolbar_box().set_sensitive(active_state)
+    journal.get_list_view().set_sensitive(active_state)
+    journal.get_volumes_toolbar().set_sensitive(active_state)
+
+
+def show_waiting_cursor():
+    # Only show waiting-cursor, if this is the batch-mode.
+
+    from jarabe.journal.journalactivity import get_journal
+    if not get_journal().is_editing_mode_present():
+        return
+
+    _journal_window.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+
+
+def freeze_ui():
+    # Only freeze, if this is the batch-mode.
+
+    from jarabe.journal.journalactivity import get_journal
+    if not get_journal().is_editing_mode_present():
+        return
+
+    show_waiting_cursor()
+
+    set_widgets_active_state(False)
+
+
+def show_normal_cursor():
+    _journal_window.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
+
+
+def unfreeze_ui():
+    # Unfreeze, irrespective of whether this is the batch mode.
+
+    set_widgets_active_state(True)
+
+    show_normal_cursor()
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
index 417ff61..67257ed 100644
--- a/src/jarabe/journal/listmodel.py
+++ b/src/jarabe/journal/listmodel.py
@@ -54,6 +54,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 +69,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 +80,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 +97,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 +121,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 f6a867f..0c3d702 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -100,6 +100,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),
@@ -136,6 +138,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)
 
@@ -244,9 +256,73 @@ 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.
+        try:
+            metadata = model.get(uid)
+        except:
+            # https://dev.laptop.org.au/issues/1119
+            # http://bugs.sugarlabs.org/ticket/3344
+            # Occurs, when copying entries from journal to pen-drive.
+            # Simply swallow the exception, and return, as this too,
+            # like the above case, does not have any impact on the
+            # functionality.
+            return
+
+        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:
+        # Instead of querying the favorite-status from the "cached"
+        # entries in listmodel, hit the DS, and retrieve the persisted
+        # favorite-status.
+        # This solves the issue in  "Multi-Select", wherein the
+        # listview is inhibited from refreshing. Now, if the user
+        # clicks favorite-star-icon(s), the change(s) is(are) written
+        # to the DS, but no refresh takes place. Thus, in order to have
+        # the change(s) reflected on the UI, we need to hit the DS for
+        # querying the favorite-status (instead of relying on the
+        # cached-listmodel.
+        uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+        if uid is None:
+            return
+
+        try:
+            metadata = model.get(uid)
+        except:
+            return
+
+        favorite = None
+        if 'keep' in metadata.keys():
+            favorite = metadata['keep']
+
+        if favorite == '1':
             client = gconf.client_get_default()
             color = XoColor(client.get_string('/desktop/sugar/user/color'))
             cell.props.xo_color = color
@@ -264,6 +340,53 @@ class BaseListView(gtk.Bin):
             metadata['keep'] = '1'
         model.write(metadata, update_mtime=False)
 
+        self.__redraw_view_if_necessary()
+
+    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()
+        journal_toolbar_box = journal.get_toolbar_box()
+
+        self.__redraw_view_if_necessary()
+
+        if new_status == False:
+            self._selected_entries = self._selected_entries - 1
+            journal_toolbar_box.process_new_deselected_entry_in_multi_select()
+            gobject.idle_add(self._post_backend_processing)
+        else:
+            self._selected_entries = self._selected_entries + 1
+            journal.get_list_view().inhibit_refresh(True)
+            journal.switch_to_editing_mode(True)
+
+            # For the case, when we are switching to editing-mode.
+            # The previous call won't actually redraw, as we are not in
+            # editing-mode that time.
+            self.__redraw_view_if_necessary()
+
+            journal.get_toolbar_box().process_new_selected_entry_in_multi_select()
+
+    def _post_backend_processing(self):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        if self._selected_entries == 0:
+            journal.switch_to_editing_mode(False)
+            journal.get_list_view().inhibit_refresh(False)
+            journal.get_list_view().refresh()
+
     def update_with_query(self, query_dict):
         logging.debug('ListView.update_with_query')
         if 'order_by' not in query_dict:
@@ -276,9 +399,15 @@ 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.set_sensitive(True)
+            self.proceed_with_refresh()
+
+    def proceed_with_refresh(self):
         logging.debug('ListView.refresh query %r', self._query)
         self._stop_progress_bar()
 
@@ -463,6 +592,64 @@ 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
+
+    def __redraw_view_if_necessary(self):
+        from jarabe.journal.journalactivity import get_journal
+        if not get_journal().is_editing_mode_present():
+            return
+
+        # First, get the total number of entries, for which the
+        # batch-operation is under progress.
+        from jarabe.journal.palettes import get_current_action_item
+
+        current_action_item = get_current_action_item()
+        if current_action_item is None:
+            # A single checkbox has been clicked/unclicked.
+            self.__redraw()
+            return
+
+        total_items = current_action_item.get_number_of_entries_to_operate_upon()
+
+        # Then, get the current entry being processed.
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+        current_entry_number = journal.get_toolbar_box().get_current_entry_number()
+
+        # Redraw, if "current_entry_number" is 10.
+        if current_entry_number == 10:
+            self.__log(current_entry_number, total_items)
+            self.__redraw()
+            return
+
+        # Redraw, if this is the last entry.
+        if current_entry_number == total_items:
+            self.__log(current_entry_number, total_items)
+            self.__redraw()
+            return
+
+        # Redraw, if this is the 20% interval.
+        twenty_percent_of_total_items = total_items / 5
+        if twenty_percent_of_total_items < 10:
+            return
+
+        if (current_entry_number % twenty_percent_of_total_items) == 0:
+            self.__log(current_entry_number, total_items)
+            self.__redraw()
+            return
+
+    def __log(self, current_entry_number, total_items):
+        pass
+
+    def __redraw(self):
+        tree_view_window = self.tree_view.get_bin_window()
+        tree_view_window.hide()
+        tree_view_window.show()
+
 
 class ListView(BaseListView):
     __gtype_name__ = 'JournalListView'
@@ -538,6 +725,10 @@ class ListView(BaseListView):
         misc.resume(metadata)
 
     def __cell_title_edited_cb(self, cell, path, new_text):
+        from jarabe.journal.journalactivity import get_journal
+        if get_journal().is_editing_mode_present():
+            return
+
         row = self._model[path]
         metadata = model.get(row[ListModel.COLUMN_UID])
         metadata['title'] = new_text
@@ -547,6 +738,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'
@@ -608,6 +810,11 @@ class CellRendererActivityIcon(CellRendererIcon):
         if not self._show_palette:
             return None
 
+        # Also, if we are in batch-operations mode, return 'None'
+        from jarabe.journal.journalactivity import get_journal
+        if get_journal().is_editing_mode_present():
+            return None
+
         tree_model = self.tree_view.get_model()
         metadata = tree_model.get_metadata(self.props.palette_invoker.path)
 
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 5285a7c..b1e3d29 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -16,6 +16,7 @@
 
 import logging
 import os
+import stat
 import errno
 import subprocess
 from datetime import datetime
@@ -433,9 +434,14 @@ def _get_file_metadata(path, stat, fetch_preview=True):
     metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview)
     if metadata:
         if 'filesize' not in metadata:
-            metadata['filesize'] = stat.st_size
+            if stat is not None:
+                metadata['filesize'] = stat.st_size
         return metadata
 
+    if stat is None:
+        raise ValueError('File does not exist')
+
+    # For Journal.
     return {'uid': path,
             'title': os.path.basename(path),
             'timestamp': stat.st_mtime,
@@ -525,8 +531,14 @@ def find(query_, page_size):
         raise ValueError('Exactly one mount point must be specified')
 
     if mount_points[0] == '/':
+        """
+        For Journal.
+        """
         return DatastoreResultSet(query, page_size)
     else:
+        """
+        For Documents/Mounted-Drives.
+        """
         return InplaceResultSet(query, page_size, mount_points[0])
 
 
@@ -543,11 +555,24 @@ def _get_mount_point(path):
 def get(object_id):
     """Returns the metadata for an object
     """
-    if os.path.exists(object_id):
-        stat = os.stat(object_id)
+    if (object_id[0] == '/'):
+        """
+        For Documents/Shares/Mounted-Drives,
+        where ".Sugar-Metadata" folder exists.
+        """
+        if os.path.exists(object_id):
+            # if the file is physically present, derive filesize-metadata
+            # by physical examination of the file.
+            stat = os.stat(object_id)
+        else:
+            stat = None
+
         metadata = _get_file_metadata(object_id, stat)
         metadata['mountpoint'] = _get_mount_point(object_id)
     else:
+        """
+        For journal, where ".Sugar-Metadata" folder does not exists.
+        """
         metadata = _get_datastore().get_properties(object_id, byte_arrays=True)
         metadata['mountpoint'] = '/'
     return metadata
@@ -557,9 +582,15 @@ def get_file(object_id):
     """Returns the file for an object
     """
     if os.path.exists(object_id):
+        """
+        For Documents/Mounted-Drives.
+        """
         logging.debug('get_file asked for file with path %r', object_id)
         return object_id
     else:
+        """
+        For Journal.
+        """
         logging.debug('get_file asked for entry with id %r', object_id)
         file_path = _get_datastore().get_filename(object_id)
         if file_path:
@@ -793,7 +824,13 @@ def is_editable(metadata):
     if metadata.get('mountpoint', '/') == '/':
         return True
     else:
-        return os.access(metadata['mountpoint'], os.W_OK)
+        # sl#3605: Instead of relying on mountpoint property being
+        #          present in the metadata, use journalactivity api.
+        #          This would work seamlessly, as "Details View' is
+        #          called, upon an entry in the context of a singular
+        #          mount-point.
+        from jarabe.journal.journalactivity import get_mount_point
+        return os.access(get_mount_point(), os.W_OK)
 
 
 def get_documents_path():
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 8fc1e5d..908d3d0 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -15,6 +15,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
 
@@ -36,7 +37,15 @@ from jarabe.model import filetransfer
 from jarabe.model import mimeregistry
 from jarabe.journal import misc
 from jarabe.journal import model
+from jarabe.journal.journalwindow import freeze_ui,           \
+                                         unfreeze_ui,         \
+                                         show_normal_cursor,  \
+                                         show_waiting_cursor
 
+friends_model = friends.get_model()
+
+_copy_menu_helper = None
+_current_action_item = None
 
 class ObjectPalette(Palette):
 
@@ -66,6 +75,9 @@ class ObjectPalette(Palette):
         Palette.__init__(self, primary_text=title,
                          icon=activity_icon)
 
+        from jarabe.journal.journalactivity import get_mount_point
+        current_mount_point = get_mount_point()
+
         if misc.get_activities(metadata) or misc.is_bundle(metadata):
             if metadata.get('activity_id', ''):
                 resume_label = _('Resume')
@@ -96,10 +108,20 @@ 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)
 
+
         if self._metadata['mountpoint'] == '/':
             menu_item = MenuItem(_('Duplicate'))
             icon = Icon(icon_name='edit-duplicate', xo_color=color,
@@ -128,6 +150,7 @@ class ObjectPalette(Palette):
         self.menu.append(menu_item)
         menu_item.show()
 
+
     def __start_activate_cb(self, menu_item):
         misc.resume(self._metadata)
 
@@ -180,120 +203,577 @@ 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
+    """
+
+    def __init__(self, label, metadata_list, show_editing_alert,
+                 show_progress_info_alert, batch_mode,
+                 auto_deselect_source_entries,
+                 need_to_popup_options,
+                 operate_on_deselected_entries,
+                 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._auto_deselect_source_entries = \
+                auto_deselect_source_entries
+        self._need_to_popup_options = \
+                need_to_popup_options
+        self._operate_on_deselected_entries = \
+                operate_on_deselected_entries
+        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._pre_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,
+                             gtk.RESPONSE_OK)
+
+    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 _pre_fill_and_pop_up_options(self, widget_clicked):
+        self._set_current_action_item_widget()
+        self._fill_and_pop_up_options(widget_clicked)
+
+    def _fill_and_pop_up_options(self, widget_clicked):
+        """
+        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.
+        """
+
+        freeze_ui()
+        gobject.idle_add(self.__show_editing_alert_after_freezing_ui,
+                         widget_clicked)
+
+    def __show_editing_alert_after_freezing_ui(self, widget_clicked):
+        self._set_current_action_item_widget()
+
+        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:
+                metadata_list = journal.get_metadata_list(False)
+            else:
+                metadata_list = journal.get_metadata_list(True)
 
-        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()
+            # Make a backup copy, of this metadata_list.
+            self._immutable_metadata_list = []
+            for metadata in metadata_list:
+                self._immutable_metadata_list.append(metadata)
 
-    def __volume_error_cb(self, menu_item, message, severity):
-        self.emit('volume-error', message, severity)
+            return metadata_list
+        else:
+            metadata_list = []
+            for metadata in self._immutable_metadata_list:
+                metadata_list.append(metadata)
+            return metadata_list
+
+    def _get_editing_alert_title(self):
+        raise NotImplementedError
+
+    def _get_editing_alert_message(self, entries_len):
+        raise NotImplementedError
+
+    def _get_editing_alert_operation(self):
+        raise NotImplementedError
+
+    def _is_metadata_list_empty(self):
+        return (self._metadata_list is None) or \
+                (len(self._metadata_list) == 0)
+
+    def _set_current_action_item_widget(self):
+        """
+        Only set this, if this widget achieves some effective action.
+        """
+        if not self._need_to_popup_options:
+            global _current_action_item
+            _current_action_item = self
+
+    def _pre_operate_per_action(self, obj, response_id):
+        """
+        This is the stage, just before the FIRST metadata gets into its
+        processing cycle.
+        """
+        freeze_ui()
+        gobject.idle_add(self.__pre_operate_per_action_after_freezing_ui,
+                         obj, response_id)
+
+    def __pre_operate_per_action_after_freezing_ui(self, obj, response_id):
+        self._set_current_action_item_widget()
+
+        self._continue_operation = True
+
+        # If the user chose to cancel the operation from the onset,
+        # simply proceeed to the last.
+        if response_id == gtk.RESPONSE_CANCEL:
+            unfreeze_ui()
+
+            self._cancel_further_batch_operation_items()
+            self._post_operate_per_action()
+            return
 
+        self._skip_all = False
 
-class VolumeMenu(MenuItem):
-    __gtype_name__ = 'JournalVolumeMenu'
+        # Also, get the initial length of the model.
+        self._model_len = self._get_list_model_len()
 
-    __gsignals__ = {
-        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
-                         ([str, str])),
-    }
+        # Speed Optimisation:
+        # ===================
+        # If the metadata-list is empty, fetch it;
+        # else we have already fetched it, when we showed the
+        # "editing-alert".
+        if len(self._metadata_list) == 0:
+            self._metadata_list = self._get_metadata_list()
 
-    def __init__(self, metadata, label, mount_point):
-        MenuItem.__init__(self, label)
-        self._metadata = metadata
-        self.connect('activate', self.__copy_to_volume_cb, mount_point)
+        # Set the initial length of metadata-list.
+        self._metadata_list_initial_len = len(self._metadata_list)
 
-    def __copy_to_volume_cb(self, menu_item, mount_point):
-        file_path = model.get_file(self._metadata['uid'])
+        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.
+        """
+
+        show_waiting_cursor()
+        gobject.idle_add(self.__pre_operate_per_metadata_per_action_after_freezing_ui)
+
+    def __pre_operate_per_metadata_per_action_after_freezing_ui(self):
+        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 of %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)
+
+            # Call the core-function !!
+            gobject.idle_add(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.
+        """
+
+        if self._continue_operation is False:
+            # Jump directly to the post-operation
+            self._post_operate_per_metadata_per_action(metadata)
+        else:
+            # 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 and self._auto_deselect_source_entries:
+            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()
+        journal_toolbar_box = journal.get_toolbar_box()
+
+        if self._batch_mode and (not self._auto_deselect_source_entries):
+            journal_toolbar_box.display_already_selected_entries_status()
+
+        self._process_switching_mode(None, False)
+
+        unfreeze_ui()
+
+        # Set the "_current_action_item" to None.
+        global _current_action_item
+        _current_action_item = None
+
+    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_confirmation_alert(self._get_info_alert_title()  + ' ...',
+                                             info_alert_message,
+                                             self._process_error_skipping,
+                                             metadata)
+        else:
+            self._process_error_skipping(metadata, gtk.RESPONSE_OK)
+
+    def _process_error_skipping(self, metadata, response_id):
+        # This sets up the decision, as to whether continue operations
+        # with the rest of the metadata.
+        if response_id == gtk.RESPONSE_CANCEL:
+            self._cancel_further_batch_operation_items()
+
+        self._post_operate_per_metadata_per_action(metadata)
+
+    def _cancel_further_batch_operation_items(self):
+        self._continue_operation = False
 
+        # Optimization:
+        # Clear the metadata-list as well.
+        # This would prevent the unnecessary traversing of the
+        # remaining checkboxes-corresponding-to-remaining-metadata (of
+        # course without doing any effective action).
+        self._metadata_list = []
+
+    def _file_path_valid(self, metadata):
+        from jarabe.journal.journalactivity import get_mount_point
+        current_mount_point = get_mount_point()
+
+        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 get_number_of_entries_to_operate_upon(self):
+        return len(self._immutable_metadata_list)
 
-    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,
+                            auto_deselect_source_entries=False,
+                            need_to_popup_options=False,
+                            operate_on_deselected_entries=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(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 _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
+
+        # This is sync-operation. Call the post-operation now.
+        self._post_operate_per_metadata_per_action(metadata)
+
 
 class FriendsMenu(gtk.Menu):
     __gtype_name__ = 'JournalFriendsMenu'
@@ -381,3 +861,94 @@ 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
+
+
+def get_current_action_item():
+    return _current_action_item
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 71b6ea8..a15145b 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -30,13 +30,17 @@ import simplejson
 import tempfile
 import shutil
 
+from sugar.graphics.toolbutton import ToolButton
 from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.icon import Icon
 from sugar.graphics.palette import Palette
 from sugar.graphics.xocolor import XoColor
 from sugar import env
 
+from jarabe.frame.notification import NotificationIcon
 from jarabe.journal import model
 from jarabe.view.palettes import VolumePalette
+import jarabe.frame
 
 
 _JOURNAL_0_METADATA_DIR = '.olpc.store'
@@ -201,12 +205,11 @@ class VolumesToolbar(gtk.Toolbar):
         for mount in volume_monitor.get_mounts():
             self._add_button(mount)
 
-    def _set_up_documents_button(self):
-        documents_path = model.get_documents_path()
-        if documents_path is not None:
-            button = DocumentsButton(documents_path)
+    def _set_up_directory_button(self, dir_path, icon_name, label_text):
+        if dir_path is not None:
+            button = DirectoryButton(dir_path, icon_name)
             button.props.group = self._volume_buttons[0]
-            label = glib.markup_escape_text(_('Documents'))
+            label = glib.markup_escape_text(label_text)
             button.set_palette(Palette(label))
             button.connect('toggled', self._button_toggled_cb)
             button.show()
@@ -216,6 +219,12 @@ class VolumesToolbar(gtk.Toolbar):
             self._volume_buttons.append(button)
             self.show()
 
+    def _set_up_documents_button(self):
+        documents_path = model.get_documents_path()
+        self._set_up_directory_button(documents_path,
+                                      'user-documents',
+                                      _('Documents'))
+
     def __mount_added_cb(self, volume_monitor, mount):
         self._add_button(mount)
 
@@ -246,8 +255,16 @@ class VolumesToolbar(gtk.Toolbar):
     def __volume_error_cb(self, button, strerror, severity):
         self.emit('volume-error', strerror, severity)
 
-    def _button_toggled_cb(self, button):
-        if button.props.active:
+    def _button_toggled_cb(self, button, force_toggle=False):
+        if button.props.active or force_toggle:
+            from jarabe.journal.journalactivity import get_journal
+            journal = get_journal()
+
+            journal.get_list_view()._selected_entries = 0
+            journal.switch_to_editing_mode(False)
+            journal.get_list_view().inhibit_refresh(False)
+            journal.get_list_view().refresh()
+
             self.emit('volume-changed', button.mount_point)
 
     def _unmount_activated_cb(self, menu_item, mount):
@@ -260,8 +277,9 @@ class VolumesToolbar(gtk.Toolbar):
     def _get_button_for_mount(self, mount):
         mount_point = mount.get_root().get_path()
         for button in self.get_children():
-            if button.mount_point == mount_point:
-                return button
+            if type(button) == VolumeButton and \
+                button.mount_point == mount_point:
+                    return button
         logging.error('Couldnt find button with mount_point %r', mount_point)
         return None
 
@@ -278,6 +296,12 @@ class VolumesToolbar(gtk.Toolbar):
         button = self._get_button_for_mount(mount)
         button.props.active = True
 
+    def get_journal_button(self):
+        return self._volume_buttons[0]
+
+    def get_button_toggled_cb(self):
+        return self._button_toggled_cb
+
 
 class BaseButton(RadioToolButton):
     __gsignals__ = {
@@ -392,12 +416,12 @@ class JournalButtonPalette(Palette):
                 {'free_space': free_space / (1024 * 1024)}
 
 
-class DocumentsButton(BaseButton):
+class DirectoryButton(BaseButton):
 
-    def __init__(self, documents_path):
-        BaseButton.__init__(self, mount_point=documents_path)
+    def __init__(self, dir_path, icon_name):
+        BaseButton.__init__(self, mount_point=dir_path)
 
-        self.props.named_icon = 'user-documents'
+        self.props.named_icon = icon_name
 
         client = gconf.client_get_default()
         color = XoColor(client.get_string('/desktop/sugar/user/color'))
-- 
1.7.4.4



More information about the Sugar-devel mailing list