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

Ajay Garg ajay at activitycentral.com
Sat Feb 11 02:42:42 EST 2012


Note that this patch must be appled in full, and not over version-1 or version-2
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.


TODO: Currently, the clock-cursor is running for select-all and
select-none, only when the running-progress is shown for them.
Need to make it work, without the running-progress :-|


 src/jarabe/journal/journalactivity.py |  128 ++++++-
 src/jarabe/journal/journaltoolbox.py  |  241 ++++++++++--
 src/jarabe/journal/listmodel.py       |    8 +
 src/jarabe/journal/listview.py        |   53 +++
 src/jarabe/journal/model.py           |   13 +-
 src/jarabe/journal/palettes.py        |  674 +++++++++++++++++++++++++++------
 src/jarabe/journal/volumestoolbar.py  |    9 +
 7 files changed, 969 insertions(+), 157 deletions(-)

diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index 8cafef0..f4bf434 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,20 @@ 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 = []
+
+        set_mount_point('/')
 
         self._setup_main_view()
         self._setup_secondary_view()
@@ -151,6 +173,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 +209,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 +242,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 +283,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):
@@ -364,8 +393,83 @@ 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)
+            if metadata.get('selected', '0') == selected_state:
+                metadata_list.append(metadata)
+
+        return metadata_list
 
 
 def get_journal():
@@ -378,3 +482,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..1daa03b 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,199 @@ 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(self, entries_len):
+        return ngettext('You have deselected %d entry.',
+                        'You have deselected %d entries.',
+                        entries_len) % (entries_len,)
+
+    def _operate(self, metadata):
+        from jarabe.journal.journalactivity import get_journal
+        journal_list_view = get_journal().get_list_view()
+
+        if not self._file_path_valid(metadata):
+            return False
+
+        metadata['selected'] = '0'
+        journal_list_view._process_new_selected_status('0')
+        model.write(metadata, update_mtime=False)
+
+        # 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(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):
+        from jarabe.journal.journalactivity import get_journal
+        journal_list_view = get_journal().get_list_view()
+
+        metadata['selected'] = '1'
+        journal_list_view._process_new_selected_status('1')
+        model.write(metadata, update_mtime=False)
+
+        # 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..613f3cf 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
@@ -198,6 +204,8 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
 
             self._cached_row.append(None)
 
+        self._cached_row.append(metadata.get('selected', '0') == '1')
+
         return self._cached_row[column]
 
     def on_iter_nth_child(self, iterator, n):
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index a0ceccc..930f554 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
@@ -98,6 +102,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 +140,18 @@ class BaseListView(gtk.Bin):
             return object_id.startswith(self._query['mountpoints'][0])
 
     def _add_columns(self):
+        cell_select = gtk.CellRendererToggle()
+        cell_select.props.indicator_size = style.zoom(26)
+        cell_select.props.activatable = True
+        cell_select.connect('toggled', self.__selected_cb)
+
+        column = gtk.TreeViewColumn()
+        column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+        column.props.fixed_width = style.GRID_CELL_SIZE
+        column.pack_start(cell_select)
+        column.add_attribute(cell_select, "active", ListModel.COLUMN_SELECT)
+        self.tree_view.append_column(column)
+
         cell_favorite = CellRendererFavorite(self.tree_view)
         cell_favorite.connect('clicked', self.__favorite_clicked_cb)
 
@@ -251,6 +269,30 @@ class BaseListView(gtk.Bin):
         else:
             cell.props.xo_color = None
 
+    def __selected_cb(self, cell, path):
+        row = self._model[path]
+        metadata = model.get(row[ListModel.COLUMN_UID])
+        if metadata.get('selected', '0') == '1':
+            metadata['selected'] = '0'
+            self._process_new_selected_status('0')
+        else:
+            metadata['selected'] = '1'
+            self._process_new_selected_status('1')
+
+        model.write(metadata, update_mtime=False)
+
+    def _process_new_selected_status(self, new_status):
+        from jarabe.journal.journalactivity import get_journal
+        journal = get_journal()
+
+        if new_status == '0':
+            self._selected_entries = self._selected_entries - 1
+            if self._selected_entries == 0:
+                journal.switch_to_editing_mode(False)
+        else:
+            self._selected_entries = self._selected_entries + 1
+            journal.switch_to_editing_mode(True)
+
     def __favorite_clicked_cb(self, cell, path):
         row = self._model[path]
         metadata = model.get(row[ListModel.COLUMN_UID])
@@ -274,9 +316,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 +513,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'
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 5285a7c..39547a4 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', 'selected']
 
 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..01cf845 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,500 @@ 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('0')
+            else:
+                return journal.get_metadata_list('1')
+        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)
+        self._inhibit_refresh(True)
 
-    def __copy_to_volume_cb(self, menu_item, mount_point):
-        file_path = model.get_file(self._metadata['uid'])
+        # Show waiting cursor (only for batch mode)
+        if self._batch_mode:
+            self.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+
+        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)
+
+            # De-select the entry.
+            # For "SelectAll", no need to set to unset an already unset
+            # entry.
+            if not self._operate_on_deselected_entries:
+                metadata['selected'] = '0'
+                get_journal().get_list_view()._process_new_selected_status('0')
+                model.write(metadata, update_mtime=False)
 
+            # 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.
+        """
+
+        # 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()
+
+        # Initially, set the operating mode to "Editing-Mode". This
+        # will be useful in the case when the volume-toolbuttons need
+        # to be activated only at the very last step.
+        journal.switch_to_editing_mode(True)
+
+        # Show post-operation confirmation message, if applicable.
+        if self._show_post_selected_confirmation:
+            entries_len = self._model_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))
+
+        self._inhibit_refresh(False)
+        journal.get_list_view().refresh()
+
+    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()
+
+        # Switch to non-editing mode (if applicable), after
+        # operation is complete.
+        if self._switch_to_normal_mode_after_completion:
+            journal.switch_to_editing_mode(False)
+
+    def _inhibit_refresh(self, inhibit):
+        if self._batch_mode:
+            from jarabe.journal.journalactivity import get_journal
+            get_journal().get_list_view().inhibit_refresh(inhibit)
+
+    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, skip_all, metadata):
+        if skip_all:
+            self._skip_all = True
+
+        # 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):
         try:
-            model.copy(self._metadata, mount_point)
+            model.copy(metadata, mount_point)
+            return True
         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'))
-
+            error_message = _('Error while copying the entry. %s') % e.strerror
+            if self._batch_mode:
+                self._handle_error_alert(error_message, metadata)
+            else:
+                self.emit('volume-error', error_message, _('Error'))
+            return False
 
-class ClipboardMenu(MenuItem):
-    __gtype_name__ = 'JournalClipboardMenu'
 
+class BaseCopyMenuItem(MenuItem, ActionItem):
     __gsignals__ = {
-        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
-                         ([str, str])),
-    }
+            'volume-error': (gobject.SIGNAL_RUN_FIRST,
+                             gobject.TYPE_NONE, ([str, str])),
+            }
 
-    def __init__(self, metadata):
-        MenuItem.__init__(self, _('Clipboard'))
-
-        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
+    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 +901,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