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

Daniel Narvaez dwnarvaez at gmail.com
Sun Apr 14 15:23:44 EDT 2013


Did you read this?

https://help.github.com/articles/using-pull-requests

You need to

* Fork the repo on the web UI
* Clone your fork
* Push the patches to your fork
* Make a pull request from the web UI

On 13 April 2013 06:13, Ajay Garg <ajay at activitycentral.com> wrote:

> Hi all.
> Any news on the progress on this front?
>
> Should I commit this on github?
>
> Daniel, Manuel : If yes, please let me know a step by step procedure for
> this.
>                          I don't want to mess up things, so I will be
> grateful if you guys could let me know the step by step procedure
>                          to get this in guthub, only one single time :)
>
>
>
> On Wed, Apr 3, 2013 at 12:22 AM, Ajay Garg <ajay at activitycentral.com>wrote:
>
>> The patch is to be applied in mainline-repo, master-branch.
>>
>>
>> The workflow is as per
>> http://wiki.sugarlabs.org/go/Features/Multi_selection_screenshots
>>
>>
>> The only known "issues" are ::
>>
>> a)
>> Journal-View is not updated, when screenshot(s) is(are) taken in
>> "Multi-Select" mode.
>>
>> ====================================================================================
>>
>> The reason is that journal-refresh is inhibited in "Multi-Select" mode.
>>
>> Thankfully, this is not too great an inconsistency, as the users are need
>> to be educated of the
>> "Alt + 1 screenshot" feature anyway, when the screenshot is taken in any
>> other view other than the
>> Journal-View. That time, the users "need" to know where they may find the
>> newly taken screenshot(s).
>>
>>
>>
>> b)
>> Renaming of entries, does not happen in "Multi-Select" mode.
>> =============================================================
>>
>> The issue is mainly in locations other than the "Journal"
>> (Documents/Pen-Drives), as renaming the entry needs
>> a corresponding change on the file-system. Due to the fact the refresh is
>> inhibited in "Multi-Select" mode,
>> this is not possible.
>>
>> Thus, with the aim of maintaining UI-consistency,
>> renaming-in-Multi-Select mode is disabled for "Journal" too.
>>
>>
>>
>>
>>  src/jarabe/journal/expandedentry.py   |  12 +-
>>  src/jarabe/journal/journalactivity.py | 182 ++++++++-
>>  src/jarabe/journal/journaltoolbox.py  | 304 +++++++++++---
>>  src/jarabe/journal/journalwindow.py   |  45 +++
>>  src/jarabe/journal/listmodel.py       |  23 ++
>>  src/jarabe/journal/listview.py        | 235 ++++++++++-
>>  src/jarabe/journal/model.py           | 243 +++++++++---
>>  src/jarabe/journal/objectchooser.py   |  22 +-
>>  src/jarabe/journal/palettes.py        | 728
>> +++++++++++++++++++++++++++++-----
>>  src/jarabe/journal/volumestoolbar.py  |  92 +++--
>>  10 files changed, 1637 insertions(+), 249 deletions(-)
>>
>> diff --git a/src/jarabe/journal/expandedentry.py
>> b/src/jarabe/journal/expandedentry.py
>> index 0cb59d6..6937370 100644
>> --- a/src/jarabe/journal/expandedentry.py
>> +++ b/src/jarabe/journal/expandedentry.py
>> @@ -554,14 +554,10 @@ class ExpandedEntry(Gtk.EventBox):
>>          self._update_title_sid = None
>>
>>      def _write_entry(self):
>> -        if self._metadata.get('mountpoint', '/') == '/':
>> -            model.write(self._metadata, update_mtime=False)
>> -        else:
>> -            old_file_path = os.path.join(
>> -                self._metadata['mountpoint'],
>> -                model.get_file_name(old_title,
>> self._metadata['mime_type']))
>> -            model.write(self._metadata, file_path=old_file_path,
>> -                        update_mtime=False)
>> +        from jarabe.journal.journalactivity import get_journal
>> +        self._metadata['mountpoint'] = \
>> +                get_journal().get_detail_toolbox().get_mount_point()
>> +
>>  model.update_only_metadata_and_preview_files_and_return_file_paths(self._metadata)
>>
>>      def _keep_icon_toggled_cb(self, keep_icon):
>>          if keep_icon.get_active():
>> diff --git a/src/jarabe/journal/journalactivity.py
>> b/src/jarabe/journal/journalactivity.py
>> index 4bb68fd..56740cf 100644
>> --- a/src/jarabe/journal/journalactivity.py
>> +++ b/src/jarabe/journal/journalactivity.py
>> @@ -19,6 +19,7 @@ import logging
>>  from gettext import gettext as _
>>  import uuid
>>
>> +from gi.repository import GObject
>>  from gi.repository import Gtk
>>  from gi.repository import Gdk
>>  from gi.repository import GdkX11
>> @@ -27,7 +28,8 @@ import statvfs
>>  import os
>>
>>  from sugar3.graphics.window import Window
>> -from sugar3.graphics.alert import ErrorAlert
>> +from sugar3.graphics.icon import Icon
>> +from sugar3.graphics.alert import Alert
>>
>>  from sugar3.bundle.bundle import ZipExtractException,
>> RegistrationException
>>  from sugar3 import env
>> @@ -37,7 +39,9 @@ from gi.repository import SugarExt
>>
>>  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
>> @@ -46,6 +50,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'
>> @@ -56,6 +61,7 @@ _SPACE_TRESHOLD = 52428800
>>  _BUNDLE_ID = 'org.laptop.JournalActivity'
>>
>>  _journal = None
>> +_mount_point = None
>>
>>
>>  class JournalActivityDBusService(dbus.service.Object):
>> @@ -124,8 +130,33 @@ 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 = Alert()
>> +        icon = Icon(icon_name='dialog-ok')
>> +        self._error_alert.add_button(Gtk.ResponseType.OK, _('Ok'), icon)
>> +        icon.show()
>> +
>> +        self._confirmation_alert = Alert()
>> +        icon = Icon(icon_name='dialog-cancel')
>> +        self._confirmation_alert.add_button(Gtk.ResponseType.CANCEL,
>> _('Stop'), icon)
>> +        icon.show()
>> +        icon = Icon(icon_name='dialog-ok')
>> +        self._confirmation_alert.add_button(Gtk.ResponseType.OK,
>> _('Continue'), icon)
>> +        icon.show()
>> +
>> +        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,10 +182,17 @@ class JournalActivity(JournalWindow):
>>          self._check_available_space()
>>
>>      def __volume_error_cb(self, gobject, message, severity):
>> -        alert = ErrorAlert(title=severity, msg=message)
>> -        alert.connect('response', self.__alert_response_cb)
>> -        self.add_alert(alert)
>> -        alert.show()
>> +        self.update_title_and_message(self._error_alert, severity,
>> +                                      message)
>> +        self._callback = None
>> +        self._data = None
>> +        self.update_alert(self._error_alert)
>> +
>> +    def _show_alert(self, message, severity):
>> +        self.__volume_error_cb(None, message, severity)
>> +
>> +    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)
>> @@ -196,11 +234,13 @@ class JournalActivity(JournalWindow):
>>          self._main_toolbox.search_entry.connect('icon-press',
>>
>>  self.__search_icon_pressed_cb)
>>          self._main_toolbox.set_mount_point('/')
>> +        set_mount_point('/')
>>
>>      def _setup_secondary_view(self):
>>          self._secondary_view = Gtk.VBox()
>>
>>          self._detail_toolbox = DetailToolbox()
>> +        self._detail_toolbox.set_mount_point('/')
>>          self._detail_toolbox.connect('volume-error',
>>                                       self.__volume_error_cb)
>>
>> @@ -240,9 +280,16 @@ class JournalActivity(JournalWindow):
>>          self.connect('key-press-event', self._key_press_event_cb)
>>
>>      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()
>> +
>> +            # TRANS: Do not translate the "%d"
>> +
>>  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)
>> @@ -277,6 +324,10 @@ class JournalActivity(JournalWindow):
>>      def __volume_changed_cb(self, volume_toolbar, mount_point):
>>          logging.debug('Selected volume: %r.', mount_point)
>>          self._main_toolbox.set_mount_point(mount_point)
>> +        set_mount_point(mount_point)
>> +
>> +        # Also, need to update the mount-point for Detail-View.
>> +        self._detail_toolbox.set_mount_point(mount_point)
>>
>>      def __model_created_cb(self, sender, **kwargs):
>>          self._check_for_bundle(kwargs['object_id'])
>> @@ -301,6 +352,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)
>> @@ -334,7 +388,13 @@ class JournalActivity(JournalWindow):
>>              return
>>
>>          metadata['bundle_id'] = bundle.get_bundle_id()
>> -        model.write(metadata)
>> +
>> +        from jarabe.journal.journalactivity import get_mount_point
>> +        metadata['mountpoint'] = get_mount_point()
>> +
>>  model.update_only_metadata_and_preview_files_and_return_file_paths(metadata)
>> +
>> +    def set_bundle_installation_allowed(self, allowed):
>> +        self._bundle_installation_allowed = allowed
>>
>>      def __window_state_event_cb(self, window, event):
>>          logging.debug('window_state_event_cb %r', self)
>> @@ -378,6 +438,102 @@ class JournalActivity(JournalWindow):
>>          self.reveal()
>>          self.show_main_view()
>>
>> +    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.get_list_view().disable_drag_and_copy()
>> +            self.show_main_view()
>> +        elif (not switch) and (self._editing_mode):
>> +            self._editing_mode = False
>> +            self.get_list_view().enable_drag_and_copy()
>> +            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.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_detail_toolbox(self):
>> +        return self._detail_toolbox
>> +
>>
>>  def get_journal():
>>      global _journal
>> @@ -389,3 +545,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 b9c5e68..77a0423 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
>> @@ -25,6 +26,7 @@ import time
>>  from gi.repository import GObject
>>  from gi.repository import Gio
>>  from gi.repository import Gtk
>> +from gi.repository import Gdk
>>
>>  from sugar3.graphics.palette import Palette
>>  from sugar3.graphics.toolbarbox import ToolbarBox
>> @@ -46,8 +48,9 @@ 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 journalwindow
>> +from jarabe.journal import journalwindow, palettes
>>
>> +COPY_MENU_HELPER = palettes.get_copy_menu_helper()
>>
>>  _AUTOSEARCH_TIMEOUT = 1000
>>
>> @@ -387,11 +390,27 @@ class DetailToolbox(ToolbarBox):
>>          separator.show()
>>
>>          erase_button = ToolButton('list-remove')
>> +        self._erase_button = erase_button
>>          erase_button.set_tooltip(_('Erase'))
>>          erase_button.connect('clicked', self._erase_button_clicked_cb)
>>          self.toolbar.insert(erase_button, -1)
>>          erase_button.show()
>>
>> +    def set_mount_point(self, mount_point):
>> +        self._mount_point = mount_point
>> +        self.set_sensitivity_of_icons()
>> +
>> +    def get_mount_point(self):
>> +        return self._mount_point
>> +
>> +    def set_sensitivity_of_icons(self):
>> +        mount_point = self.get_mount_point()
>> +        sensitivity = True
>> +
>> +        self._resume.set_sensitive(sensitivity)
>> +        self._duplicate.set_sensitive(sensitivity)
>> +        self._erase_button.set_sensitive(sensitivity)
>> +
>>      def set_metadata(self, metadata):
>>          self._metadata = metadata
>>          self._refresh_copy_palette()
>> @@ -449,50 +468,11 @@ class DetailToolbox(ToolbarBox):
>>              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.IconSize.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.IconSize.MENU))
>> -            journal_menu.connect('volume-error', self.__volume_error_cb)
>> -            palette.menu.append(journal_menu)
>> -            journal_menu.show()
>> -
>> -        documents_path = model.get_documents_path()
>> -        if documents_path is not None and not \
>> -                self._metadata['uid'].startswith(documents_path):
>> -            documents_menu = VolumeMenu(self._metadata, _('Documents'),
>> -                                        documents_path)
>> -            documents_menu.set_image(Icon(icon_name='user-documents',
>> -                                          icon_size=Gtk.IconSize.MENU))
>> -            documents_menu.connect('volume-error',
>> self.__volume_error_cb)
>> -            palette.menu.append(documents_menu)
>> -            documents_menu.show()
>> -
>> -        volume_monitor = Gio.VolumeMonitor.get()
>> -        icon_theme = Gtk.IconTheme.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.IconSize.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)
>> @@ -532,6 +512,240 @@ class DetailToolbox(ToolbarBox):
>>              menu_item.show()
>>
>>
>> +class EditToolbox(ToolbarBox):
>> +    def __init__(self):
>> +        ToolbarBox.__init__(self)
>> +
>> +        self.toolbar.add(SelectNoneButton())
>> +        self.toolbar.add(SelectAllButton())
>> +
>> +        self.toolbar.add(Gtk.SeparatorToolItem())
>> +
>> +        self.toolbar.add(BatchEraseButton())
>> +        self.toolbar.add(BatchCopyButton())
>> +
>> +        self.toolbar.add(Gtk.SeparatorToolItem())
>> +
>> +        self._multi_select_info_widget = MultiSelectEntriesInfoWidget()
>> +        self.toolbar.add(self._multi_select_info_widget)
>> +
>> +        self.show_all()
>> +        self.toolbar.show_all()
>> +
>> +    def process_new_selected_entry_in_multi_select(self):
>> +        GObject.idle_add(self._multi_select_info_widget.update_text,
>> +                         '', '', True, True)
>> +
>> +    def process_new_deselected_entry_in_multi_select(self):
>> +        GObject.idle_add(self._multi_select_info_widget.update_text,
>> +                         '', '', False, True)
>> +
>> +    def display_running_status_in_multi_select(self, primary_info,
>> +                                               secondary_info):
>> +        GObject.idle_add(self._multi_select_info_widget.update_text,
>> +                         primary_info, secondary_info,
>> +                         None, None)
>> +
>> +    def display_already_selected_entries_status(self):
>> +        GObject.idle_add(self._multi_select_info_widget.update_text,
>> +                         '', '', True, False)
>> +
>> +    def set_total_number_of_entries(self, total):
>> +        self._multi_select_info_widget.set_total_number_of_entries(total)
>> +
>> +    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
>> +        self._fill_and_pop_up_options(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)
>> +        if widget_clicked is not None:
>> +            self.props.palette.popup(immediate=True, state=1)
>> +
>> +
>> +class MultiSelectEntriesInfoWidget(Gtk.ToolItem):
>> +    def __init__(self):
>> +        Gtk.ToolItem.__init__(self)
>> +
>> +        self._box = Gtk.VBox()
>> +        self._selected_entries = 0
>> +
>> +        self._label = Gtk.Label()
>> +        self._box.pack_start(self._label, True, True, 0)
>> +
>> +        self.add(self._box)
>> +
>> +        self.show_all()
>> +        self._box.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()
>> +
>> +        Gdk.Window.process_all_updates()
>> +
>> +    def get_current_entry_number(self):
>> +        return self._selected_entries
>> +
>> +
>>  class SortingButton(ToolButton):
>>      __gtype_name__ = 'JournalSortingButton'
>>
>> diff --git a/src/jarabe/journal/journalwindow.py
>> b/src/jarabe/journal/journalwindow.py
>> index 776a495..8fcecaf 100644
>> --- a/src/jarabe/journal/journalwindow.py
>> +++ b/src/jarabe/journal/journalwindow.py
>> @@ -15,6 +15,8 @@
>>  # along with this program; if not, write to the Free Software
>>  # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
>>  USA
>>
>> +from gi.repository import Gdk
>> +
>>  from sugar3.graphics.window import Window
>>
>>  _journal_window = None
>> @@ -31,3 +33,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(Gdk.Cursor.new(Gdk.CursorType.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(Gdk.Cursor.new(Gdk.CursorType.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 b98d01c..a5bb7b0 100644
>> --- a/src/jarabe/journal/listmodel.py
>> +++ b/src/jarabe/journal/listmodel.py
>> @@ -54,6 +54,7 @@ class ListModel(GObject.GObject, Gtk.TreeModel,
>> 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(GObject.GObject, Gtk.TreeModel,
>> Gtk.TreeDragSource):
>>          COLUMN_BUDDY_1: object,
>>          COLUMN_BUDDY_3: object,
>>          COLUMN_BUDDY_2: object,
>> +        COLUMN_SELECT: bool,
>>      }
>>
>>      _PAGE_SIZE = 10
>> @@ -79,6 +81,8 @@ class ListModel(GObject.GObject, Gtk.TreeModel,
>> Gtk.TreeDragSource):
>>          self._cached_row = None
>>          self._result_set = model.find(query, ListModel._PAGE_SIZE)
>>          self._temp_drag_file_path = None
>> +        self._selected = {}
>> +        self._uid_metadata_assoc = {}
>>
>>          # HACK: The view will tell us that it is resizing so the model
>> can
>>          # avoid hitting D-Bus and disk.
>> @@ -248,3 +252,22 @@ class ListModel(GObject.GObject, Gtk.TreeModel,
>> Gtk.TreeDragSource):
>>              return True
>>
>>          return False
>> +
>> +    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 get_in_memory_metadata(self, path):
>> +        uid = self[path][ListModel.COLUMN_UID]
>> +        return self._uid_metadata_assoc[uid]
>> diff --git a/src/jarabe/journal/listview.py
>> b/src/jarabe/journal/listview.py
>> index 3356d6f..32cbc2f 100644
>> --- a/src/jarabe/journal/listview.py
>> +++ b/src/jarabe/journal/listview.py
>> @@ -70,7 +70,8 @@ class BaseListView(Gtk.Bin):
>>          'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])),
>>      }
>>
>> -    def __init__(self):
>> +    def __init__(self, is_object_chooser):
>> +        self._is_object_chooser = is_object_chooser
>>          self._query = {}
>>          self._model = None
>>          self._progress_bar = None
>> @@ -101,11 +102,9 @@ class BaseListView(Gtk.Bin):
>>          self._title_column = None
>>          self.sort_column = None
>>          self._add_columns()
>> -
>> -
>>  self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
>> -                                                [('text/uri-list', 0, 0),
>> -                                                 ('journal-object-id',
>> 0, 0)],
>> -                                                Gdk.DragAction.COPY)
>> +        self._inhibit_refresh = False
>> +        self._selected_entries = 0
>> +        self.enable_drag_and_copy()
>>
>>          # Auto-update stuff
>>          self._fully_obscured = True
>> @@ -117,6 +116,15 @@ class BaseListView(Gtk.Bin):
>>          model.updated.connect(self.__model_updated_cb)
>>          model.deleted.connect(self.__model_deleted_cb)
>>
>> +    def enable_drag_and_copy(self):
>> +
>>  self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
>> +                                                [('text/uri-list', 0, 0),
>> +                                                 ('journal-object-id',
>> 0, 0)],
>> +                                                Gdk.DragAction.COPY)
>> +
>> +    def disable_drag_and_copy(self):
>> +        self.tree_view.unset_rows_drag_source()
>> +
>>      def __model_created_cb(self, sender, signal, object_id):
>>          if self._is_new_item_visible(object_id):
>>              self._set_dirty()
>> @@ -137,6 +145,17 @@ class BaseListView(Gtk.Bin):
>>              return object_id.startswith(self._query['mountpoints'][0])
>>
>>      def _add_columns(self):
>> +        if not self._is_object_chooser:
>> +            cell_select = CellRendererToggle(self.tree_view)
>> +            cell_select.connect('clicked', self.__cell_select_clicked_cb)
>> +
>> +            column = Gtk.TreeViewColumn()
>> +            column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
>> +            column.props.fixed_width = cell_select.props.width
>> +            column.pack_start(cell_select, True)
>> +            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)
>>
>> @@ -262,8 +281,30 @@ class BaseListView(Gtk.Bin):
>>
>>      def __favorite_set_data_cb(self, column, cell, tree_model,
>>                                 tree_iter, data):
>> -        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 = str(metadata['keep'])
>> +
>> +        if favorite == '1':
>>              client = GConf.Client.get_default()
>>              color =
>> XoColor(client.get_string('/desktop/sugar/user/color'))
>>              cell.props.xo_color = color
>> @@ -279,7 +320,94 @@ class BaseListView(Gtk.Bin):
>>              metadata['keep'] = '0'
>>          else:
>>              metadata['keep'] = '1'
>> -        model.write(metadata, update_mtime=False)
>> +
>> +        from jarabe.journal.journalactivity import get_mount_point
>> +        metadata['mountpoint'] = get_mount_point()
>> +
>> +
>>  model.update_only_metadata_and_preview_files_and_return_file_paths(metadata)
>> +        self.__redraw_view_if_necessary()
>> +
>> +    def __select_set_data_cb(self, column, cell, tree_model, tree_iter,
>> +                             data):
>> +        uid = tree_model[tree_iter][ListModel.COLUMN_UID]
>> +        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 __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_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 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')
>> @@ -296,6 +424,11 @@ class BaseListView(Gtk.Bin):
>>          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()
>>
>> @@ -497,6 +630,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'
>> @@ -512,8 +703,8 @@ class ListView(BaseListView):
>>                                  ([])),
>>      }
>>
>> -    def __init__(self):
>> -        BaseListView.__init__(self)
>> +    def __init__(self, is_object_chooser=False):
>> +        BaseListView.__init__(self, is_object_chooser)
>>          self._is_dragging = False
>>
>>          self.tree_view.connect('drag-begin', self.__drag_begin_cb)
>> @@ -579,6 +770,11 @@ class ListView(BaseListView):
>>          misc.resume(metadata)
>>
>>      def __cell_title_edited_cb(self, cell, path, new_text):
>> +        from jarabe.journal.journalactivity import get_journal, \
>> +                                                   get_mount_point
>> +        if get_journal().is_editing_mode_present():
>> +            return
>> +
>>          row = self._model[path]
>>          metadata = model.get(row[ListModel.COLUMN_UID])
>>          metadata['title'] = new_text
>> @@ -607,6 +803,18 @@ class CellRendererFavorite(CellRendererIcon):
>>          self.props.prelit_stroke_color = prelit_color.get_stroke_color()
>>          self.props.prelit_fill_color = prelit_color.get_fill_color()
>>
>> +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.CellRendererMode.ACTIVATABLE
>> +
>>
>>  class CellRendererDetail(CellRendererIcon):
>>      __gtype_name__ = 'JournalCellRendererDetail'
>> @@ -651,6 +859,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 0a5b354..f1fe931 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
>> @@ -58,6 +59,16 @@ updated = dispatch.Signal()
>>  deleted = dispatch.Signal()
>>
>>
>> +def _get_mount_point(path):
>> +    dir_path = os.path.dirname(path)
>> +    while dir_path:
>> +        if os.path.ismount(dir_path):
>> +            return dir_path
>> +        else:
>> +            dir_path = dir_path.rsplit(os.sep, 1)[0]
>> +    return None
>> +
>> +
>>  class _Cache(object):
>>
>>      __gtype_name__ = 'model_Cache'
>> @@ -201,6 +212,14 @@ class BaseResultSet(object):
>>
>>          return self._cache[self._position - self._offset]
>>
>> +    def is_favorite_compatible(self, metadata):
>> +        if self._favorite == '0':
>> +            return True
>> +
>> +        return ((metadata is not None) and \
>> +                ('keep' in metadata.keys()) and \
>> +                (str(metadata['keep']) == '1'))
>> +
>>
>>  class DatastoreResultSet(BaseResultSet):
>>      """Encapsulates the result of a query on the datastore
>> @@ -264,6 +283,8 @@ class InplaceResultSet(BaseResultSet):
>>
>>          self._sort = query.get('order_by', ['+timestamp'])[0]
>>
>> +        self._favorite = str(query.get('keep', 0))
>> +
>>      def setup(self):
>>          self._file_list = []
>>          self._pending_directories = [self._mount_point]
>> @@ -371,10 +392,13 @@ class InplaceResultSet(BaseResultSet):
>>          if S_IFMT(stat.st_mode) != S_IFREG:
>>              return
>>
>> +        metadata = _get_file_metadata(full_path, stat,
>> +                                      fetch_preview=False)
>> +
>> +        if not self.is_favorite_compatible(metadata):
>> +            return
>>          if self._regex is not None and \
>>                  not self._regex.match(full_path):
>> -            metadata = _get_file_metadata(full_path, stat,
>> -                                          fetch_preview=False)
>>              if not metadata:
>>                  return
>>              add_to_list = False
>> @@ -434,9 +458,13 @@ 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')
>> +
>>      mime_type, uncertain_result_ = Gio.content_type_guess(filename=path,
>>                                                            data=None)
>>      return {'uid': path,
>> @@ -457,10 +485,17 @@ def _get_file_metadata_from_json(dir_path,
>> filename, fetch_preview):
>>      If the metadata is corrupted we do remove it and the preview as well.
>>
>>      """
>> +
>> +    # In case of nested mount-points, (eg. ~/Documents/in1/in2/in3.txt),
>> +    # "dir_path = ~/Documents/in1/in2"; while
>> +    # "metadata_dir_path = ~/Documents".
>> +    from jarabe.journal.journalactivity import get_mount_point
>> +    metadata_dir_path = get_mount_point()
>> +
>>      metadata = None
>> -    metadata_path = os.path.join(dir_path, JOURNAL_METADATA_DIR,
>> +    metadata_path = os.path.join(metadata_dir_path, JOURNAL_METADATA_DIR,
>>                                   filename + '.metadata')
>> -    preview_path = os.path.join(dir_path, JOURNAL_METADATA_DIR,
>> +    preview_path = os.path.join(metadata_dir_path, JOURNAL_METADATA_DIR,
>>                                  filename + '.preview')
>>
>>      if not os.path.exists(metadata_path):
>> @@ -546,8 +581,12 @@ 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] == '/'):
>> +        if os.path.exists(object_id):
>> +            stat = os.stat(object_id)
>> +        else:
>> +            stat = None
>> +
>>          metadata = _get_file_metadata(object_id, stat)
>>          metadata['mountpoint'] = _get_mount_point(object_id)
>>      else:
>> @@ -620,7 +659,12 @@ def delete(object_id):
>>  def copy(metadata, mount_point):
>>      """Copies an object to another mount point
>>      """
>> +    # In all cases, "copy" means the actual duplication of
>> +    # the content.
>> +    transfer_ownership = False
>> +
>>      metadata = get(metadata['uid'])
>> +
>>      if mount_point == '/' and metadata['icon-color'] ==
>> '#000000,#ffffff':
>>          client = GConf.Client.get_default()
>>          metadata['icon-color'] =
>> client.get_string('/desktop/sugar/user/color')
>> @@ -631,7 +675,7 @@ def copy(metadata, mount_point):
>>      metadata['mountpoint'] = mount_point
>>      del metadata['uid']
>>
>> -    return write(metadata, file_path, transfer_ownership=False)
>> +    return write(metadata, file_path,
>> transfer_ownership=transfer_ownership)
>>
>>
>>  def write(metadata, file_path='', update_mtime=True,
>> transfer_ownership=True):
>> @@ -654,21 +698,45 @@ def write(metadata, file_path='',
>> update_mtime=True, transfer_ownership=True):
>>                                                   file_path,
>>                                                   transfer_ownership)
>>      else:
>> -        object_id = _write_entry_on_external_device(metadata, file_path)
>> +        object_id = _write_entry_on_external_device(metadata,
>> +                                                    file_path,
>> +                                                    transfer_ownership)
>>
>>      return object_id
>>
>>
>> -def _rename_entry_on_external_device(file_path, destination_path,
>> -                                     metadata_dir_path):
>> +def make_file_fully_permissible(file_path):
>> +    fd = os.open(file_path, os.O_RDONLY)
>> +    os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG |stat.S_IRWXO)
>> +    os.close(fd)
>> +
>> +
>> +def _rename_entry_on_external_device(file_path, destination_path):
>>      """Rename an entry with the associated metadata on an external
>> device."""
>>      old_file_path = file_path
>>      if old_file_path != destination_path:
>> -        os.rename(file_path, destination_path)
>> +        # Strangely, "os.rename" works fine on sugar-jhbuild, but fails
>> +        # on XOs, wih the OSError 13 ("invalid cross-device link"). So,
>> +        # using the system call "mv".
>> +        os.system('mv "%s" "%s"' % (file_path, destination_path))
>> +        make_file_fully_permissible(destination_path)
>> +
>> +
>> +        # In renaming, we want to delete the metadata-, and preview-
>> +        # files of the current mount-point, and not the destination
>> +        # mount-point.
>> +        # But we also need to ensure that the directory of
>> +        # 'old_file_path' and 'destination_path' are not same.
>> +        if os.path.dirname(old_file_path) ==
>> os.path.dirname(destination_path):
>> +            return
>> +
>> +        from jarabe.journal.journalactivity import get_mount_point
>> +        source_metadata_dir_path = get_mount_point() + '/.Sugar-Metadata'
>> +
>>          old_fname = os.path.basename(file_path)
>> -        old_files = [os.path.join(metadata_dir_path,
>> +        old_files = [os.path.join(source_metadata_dir_path,
>>                                    old_fname + '.metadata'),
>> -                     os.path.join(metadata_dir_path,
>> +                     os.path.join(source_metadata_dir_path,
>>                                    old_fname + '.preview')]
>>          for ofile in old_files:
>>              if os.path.exists(ofile):
>> @@ -679,42 +747,27 @@ def _rename_entry_on_external_device(file_path,
>> destination_path,
>>                                    'for file=%s', ofile, old_fname)
>>
>>
>> -def _write_entry_on_external_device(metadata, file_path):
>> -    """Create and update an entry copied from the
>> -    DS to an external storage device.
>> -
>> -    Besides copying the associated file a file for the preview
>> -    and one for the metadata are stored in the hidden directory
>> -    .Sugar-Metadata.
>> -
>> -    This function handles renames of an entry on the
>> -    external device and avoids name collisions. Renames are
>> -    handled failsafe.
>> -
>> -    """
>> -    if 'uid' in metadata and os.path.exists(metadata['uid']):
>> -        file_path = metadata['uid']
>> -
>> -    if not file_path or not os.path.exists(file_path):
>> -        raise ValueError('Entries without a file cannot be copied to '
>> -                         'removable devices')
>> -
>> -    if not metadata.get('title'):
>> -        metadata['title'] = _('Untitled')
>> -    file_name = get_file_name(metadata['title'], metadata['mime_type'])
>> -
>> -    destination_path = os.path.join(metadata['mountpoint'], file_name)
>> -    if destination_path != file_path:
>> -        file_name = get_unique_file_name(metadata['mountpoint'],
>> file_name)
>> -        destination_path = os.path.join(metadata['mountpoint'],
>> file_name)
>> -        clean_name, extension_ = os.path.splitext(file_name)
>> -        metadata['title'] = clean_name
>> -
>> +def _write_metadata_and_preview_files_and_return_file_paths(metadata,
>> +                                                            file_name):
>>      metadata_copy = metadata.copy()
>>      metadata_copy.pop('mountpoint', None)
>>      metadata_copy.pop('uid', None)
>>      metadata_copy.pop('filesize', None)
>>
>> +    # For journal case, there is the special treatment.
>> +    if metadata.get('mountpoint', '/') == '/':
>> +        if metadata.get('uid', ''):
>> +            object_id = _get_datastore().update(metadata['uid'],
>> +
>>  dbus.Dictionary(metadata),
>> +                                                '',
>> +                                                False)
>> +        else:
>> +            object_id =
>> _get_datastore().create(dbus.Dictionary(metadata),
>> +                                                '',
>> +                                                False)
>> +        return
>> +
>> +
>>      metadata_dir_path = os.path.join(metadata['mountpoint'],
>>                                       JOURNAL_METADATA_DIR)
>>      if not os.path.exists(metadata_dir_path):
>> @@ -742,11 +795,77 @@ def _write_entry_on_external_device(metadata,
>> file_path):
>>              os.close(fh)
>>              os.rename(fn, os.path.join(metadata_dir_path, preview_fname))
>>
>> -    if not os.path.dirname(destination_path) ==
>> os.path.dirname(file_path):
>> -        shutil.copy(file_path, destination_path)
>> +    metadata_destination_path = os.path.join(metadata_dir_path,
>> file_name + '.metadata')
>> +    make_file_fully_permissible(metadata_destination_path)
>> +    if preview:
>> +        preview_destination_path =  os.path.join(metadata_dir_path,
>> preview_fname)
>> +        make_file_fully_permissible(preview_destination_path)
>>      else:
>> -        _rename_entry_on_external_device(file_path, destination_path,
>> -                                         metadata_dir_path)
>> +        preview_destination_path = None
>> +
>> +    return (metadata_destination_path, preview_destination_path)
>> +
>> +
>> +def
>> update_only_metadata_and_preview_files_and_return_file_paths(metadata):
>> +    """
>> +
>> +    This function replaces the following paradigm for updating just the
>> +    metadata ::
>> +
>> +        def write(metadata, file_path, update_mtime=False)
>> +
>> +
>> +    Using the new API serves the following purpose ::
>> +        * Clearer name <===> Clearer intention
>> +        * Just one argument ("metadata")
>> +
>> +    """
>> +
>> +    file_name = metadata['title']
>> +    _write_metadata_and_preview_files_and_return_file_paths(metadata,
>> +                                                            file_name)
>> +
>> +
>> +def _write_entry_on_external_device(metadata, file_path,
>> +                                    transfer_ownership):
>> +    """Create and update an entry copied from the
>> +    DS to an external storage device.
>> +
>> +    Besides copying the associated file a file for the preview
>> +    and one for the metadata are stored in the hidden directory
>> +    .Sugar-Metadata.
>> +
>> +    This function handles renames of an entry on the
>> +    external device and avoids name collisions. Renames are
>> +    handled failsafe.
>> +
>> +    """
>> +    if 'uid' in metadata and os.path.exists(metadata['uid']):
>> +        file_path = metadata['uid']
>> +
>> +    if not file_path or not os.path.exists(file_path):
>> +        raise ValueError('Entries without a file cannot be copied to '
>> +                         'removable devices')
>> +
>> +    if not metadata.get('title'):
>> +        metadata['title'] = _('Untitled')
>> +    file_name = get_file_name(metadata['title'], metadata['mime_type'])
>> +
>> +    destination_path = os.path.join(metadata['mountpoint'], file_name)
>> +    if destination_path != file_path:
>> +        file_name = get_unique_file_name(metadata['mountpoint'],
>> file_name)
>> +        destination_path = os.path.join(metadata['mountpoint'],
>> file_name)
>> +        metadata['title'] = file_name
>> +
>> +    _write_metadata_and_preview_files_and_return_file_paths(metadata,
>> +                                                            file_name)
>> +
>> +    if (os.path.dirname(destination_path) == os.path.dirname(file_path))
>> or \
>> +       (transfer_ownership == True):
>> +        _rename_entry_on_external_device(file_path, destination_path)
>> +    else:
>> +        shutil.copy(file_path, destination_path)
>> +        make_file_fully_permissible(destination_path)
>>
>>      object_id = destination_path
>>      created.send(None, object_id=object_id)
>> @@ -757,12 +876,6 @@ def _write_entry_on_external_device(metadata,
>> file_path):
>>  def get_file_name(title, mime_type):
>>      file_name = title
>>
>> -    extension = mime.get_primary_extension(mime_type)
>> -    if extension is not None and extension:
>> -        extension = '.' + extension
>> -        if not file_name.endswith(extension):
>> -            file_name += extension
>> -
>>      # Invalid characters in VFAT filenames. From
>>      # http://en.wikipedia.org/wiki/File_Allocation_Table
>>      invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|',
>> '\x7F']
>> @@ -770,11 +883,11 @@ def get_file_name(title, mime_type):
>>      for char in invalid_chars:
>>          file_name = file_name.replace(char, '_')
>>
>> -    # FAT limit is 255, leave some space for uniqueness
>> -    max_len = 250
>> -    if len(file_name) > max_len:
>> -        name, extension = os.path.splitext(file_name)
>> -        file_name = name[0:max_len - len(extension)] + extension
>> +    extension = mime.get_primary_extension(mime_type)
>> +    if extension is not None and extension:
>> +        extension = '.' + extension
>> +        if not file_name.endswith(extension):
>> +            file_name += extension
>>
>>      return file_name
>>
>> @@ -796,7 +909,15 @@ 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
>> +        mount_point = get_mount_point()
>> +
>> +        return os.access(mount_point, os.W_OK)
>>
>>
>>  def get_documents_path():
>> diff --git a/src/jarabe/journal/objectchooser.py
>> b/src/jarabe/journal/objectchooser.py
>> index 9315192..ccee840 100644
>> --- a/src/jarabe/journal/objectchooser.py
>> +++ b/src/jarabe/journal/objectchooser.py
>> @@ -16,15 +16,20 @@
>>
>>  from gettext import gettext as _
>>  import logging
>> +import os
>>
>>  from gi.repository import GObject
>>  from gi.repository import Gtk
>>  from gi.repository import Gdk
>>  from gi.repository import Wnck
>>
>> +from sugar3 import env
>> +
>>  from sugar3.graphics import style
>>  from sugar3.graphics.toolbutton import ToolButton
>>
>> +from sugar3.datastore import datastore
>> +
>>  from jarabe.journal.listview import BaseListView
>>  from jarabe.journal.listmodel import ListModel
>>  from jarabe.journal.journaltoolbox import MainToolbox
>> @@ -48,6 +53,7 @@ class ObjectChooser(Gtk.Window):
>>          self.set_has_resize_grip(False)
>>
>>          self._selected_object_id = None
>> +        self._callback = None
>>
>>          self.add_events(Gdk.EventMask.VISIBILITY_NOTIFY_MASK)
>>          self.connect('visibility-notify-event',
>> @@ -112,6 +118,15 @@ class ObjectChooser(Gtk.Window):
>>          self._selected_object_id = uid
>>          self.emit('response', Gtk.ResponseType.ACCEPT)
>>
>> +        if self._callback is not None:
>> +            self._callback(self._selected_object_id)
>> +
>> +    def get_selected_object(self):
>> +        if self._selected_object_id is None:
>> +            return None
>> +        else:
>> +            return datastore.get(self._selected_object_id)
>> +
>>      def __delete_event_cb(self, chooser, event):
>>          self.emit('response', Gtk.ResponseType.DELETE_EVENT)
>>
>> @@ -122,6 +137,8 @@ class ObjectChooser(Gtk.Window):
>>
>>      def __close_button_clicked_cb(self, button):
>>          self.emit('response', Gtk.ResponseType.DELETE_EVENT)
>> +        if self._callback is not None:
>> +            self._callback(self._selected_object_id)
>>
>>      def get_selected_object_id(self):
>>          return self._selected_object_id
>> @@ -141,6 +158,9 @@ class ObjectChooser(Gtk.Window):
>>      def __clear_clicked_cb(self, list_view):
>>          self._toolbar.clear_query()
>>
>> +    def _set_callback(self, callback):
>> +        self._callback = callback
>> +
>>
>>  class TitleBox(VolumesToolbar):
>>      __gtype_name__ = 'TitleBox'
>> @@ -179,7 +199,7 @@ class ChooserListView(BaseListView):
>>      }
>>
>>      def __init__(self):
>> -        BaseListView.__init__(self)
>> +        BaseListView.__init__(self, True)
>>
>>          self.cell_icon.props.show_palette = False
>>          self.tree_view.props.hover_selection = True
>> diff --git a/src/jarabe/journal/palettes.py
>> b/src/jarabe/journal/palettes.py
>> index c675c6a..c790bc0 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
>>
>> @@ -39,6 +40,15 @@ from jarabe.model import mimeregistry
>>  from jarabe.journal import misc
>>  from jarabe.journal import model
>>  from jarabe.journal import journalwindow
>> +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):
>> @@ -100,6 +110,15 @@ class ObjectPalette(Palette):
>>          self.menu.append(menu_item)
>>          menu_item.show()
>>          copy_menu = CopyMenu(metadata)
>> +        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)
>>
>> @@ -198,134 +217,576 @@ class CopyMenu(Gtk.Menu):
>>
>>      __gsignals__ = {
>>          'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
>> -                         ([str, str])),
>> +                        ([str, str])),
>>      }
>>
>>      def __init__(self, metadata):
>>          Gtk.Menu.__init__(self)
>>
>> -        self._metadata = metadata
>>
>> -        clipboard_menu = ClipboardMenu(self._metadata)
>> -        clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
>> -                                      icon_size=Gtk.IconSize.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
>> +    """
>> +    __gtype_name__ = 'JournalActionItem'
>> +
>> +    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)
>> +
>> +        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.ResponseType.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)
>>
>> -        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.IconSize.MENU))
>> -            journal_menu.connect('volume-error', self.__volume_error_cb)
>> -            self.append(journal_menu)
>> -            journal_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)
>>
>> -        documents_path = model.get_documents_path()
>> -        if documents_path is not None and not \
>> -                self._metadata['uid'].startswith(documents_path):
>> -            documents_menu = VolumeMenu(self._metadata, _('Documents'),
>> -                                        documents_path)
>> -            documents_menu.set_image(Icon(icon_name='user-documents',
>> -                                          icon_size=Gtk.IconSize.MENU))
>> -            documents_menu.connect('volume-error',
>> self.__volume_error_cb)
>> -            self.append(documents_menu)
>> -            documents_menu.show()
>> +            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_done_ui_freezing,
>> +                         obj, response_id)
>> +
>> +    def _pre_operate_per_action_after_done_ui_freezing(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.ResponseType.CANCEL:
>> +            unfreeze_ui()
>> +
>> +            self._cancel_further_batch_operation_items()
>> +            self._post_operate_per_action()
>> +            return
>>
>> -        volume_monitor = Gio.VolumeMonitor.get()
>> -        icon_theme = Gtk.IconTheme.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.IconSize.MENU))
>> -                    break
>> -            volume_menu.connect('volume-error', self.__volume_error_cb)
>> -            self.append(volume_menu)
>> -            volume_menu.show()
>> +        self._skip_all = False
>>
>> -    def __volume_error_cb(self, menu_item, message, severity):
>> -        self.emit('volume-error', message, severity)
>> +        # Also, get the initial length of the model.
>> +        self._model_len = self._get_list_model_len()
>>
>> +        # 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()
>>
>> -class VolumeMenu(MenuItem):
>> -    __gtype_name__ = 'JournalVolumeMenu'
>> +        # Set the initial length of metadata-list.
>> +        self._metadata_list_initial_len = len(self._metadata_list)
>>
>> -    __gsignals__ = {
>> -        'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
>> -                         ([str, str])),
>> -    }
>> +        self._metadata_processed = 0
>>
>> -    def __init__(self, metadata, label, mount_point):
>> -        MenuItem.__init__(self, label)
>> -        self._metadata = metadata
>> -        self.connect('activate', self.__copy_to_volume_cb, mount_point)
>> +        # Next, proceed with the metadata
>> +        self._pre_operate_per_metadata_per_action()
>>
>> -    def __copy_to_volume_cb(self, menu_item, mount_point):
>> -        file_path = model.get_file(self._metadata['uid'])
>> +    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,
>> +                                              response_id=None):
>> +        """
>> +        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_single_mode_notification(self, message, severity):
>> +        from jarabe.journal.journalactivity import get_journal
>> +        journal = get_journal()
>> +
>> +        journal._show_alert(message, severity)
>> +
>> +    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)
>> +
>> +        # 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(_('Error'),
>> +                                             error_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.ResponseType.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):
>> +        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._handle_single_mode_notification(error_message,
>> _('Warning'))
>> +            return False
>> +        else:
>> +            return True
>> +
>> +    def _copy_entry_and_check_status(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(e)
>> +            error_message = _('Error while copying the entry. %s') % e
>> +            if self._batch_mode:
>> +                self._handle_error_alert(error_message, metadata)
>> +            else:
>> +                self._handle_single_mode_notification(error_message,
>> _('Error'))
>> +            return False
>> +        finally:
>> +            self._set_bundle_installation_allowed(True)
>> +
>> +    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()
>>
>> +        if self._batch_mode:
>> +            journal.set_bundle_installation_allowed(allowed)
>>
>> -class ClipboardMenu(MenuItem):
>> -    __gtype_name__ = 'JournalClipboardMenu'
>> +    def get_number_of_entries_to_operate_upon(self):
>> +        return len(self._immutable_metadata_list)
>> +
>> +
>> +class BaseCopyMenuItem(MenuItem, ActionItem):
>> +    __gtype_name__ = 'JournalBaseCopyMenuItem'
>>
>>      __gsignals__ = {
>> -        'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
>> -                         ([str, str])),
>> -    }
>> +            'volume-error': (GObject.SignalFlags.RUN_FIRST,
>> +                             None, ([str, str])),
>> +            }
>>
>> -    def __init__(self, metadata):
>> -        MenuItem.__init__(self, _('Clipboard'))
>> +    def __init__(self, metadata_list, label, show_editing_alert,
>> +                 show_progress_info_alert, batch_mode, mount_point):
>> +        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)
>> +        self._mount_point = mount_point
>>
>> -        self._temp_file_path = None
>> -        self._metadata = metadata
>> -        self.connect('activate', self.__copy_to_clipboard_cb)
>> +    def get_mount_point(self):
>> +        return self._mount_point
>>
>> -    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 _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')
>> +
>> +    def _operate(self, metadata):
>> +        self._proceed_with_copy(metadata)
>> +
>> +    def _proceed_with_copy(self, metadata):
>> +        return NotImplementedError
>> +
>> +    def _post_successful_copy(self, metadata, response_id=None):
>> +        self._post_operate_per_metadata_per_action(metadata)
>> +
>> +
>> +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,
>> +                                  mount_point)
>> +
>> +    def _proceed_with_copy(self, metadata):
>> +        if not self._file_path_valid(metadata):
>> +            return False
>> +
>> +        if not self._copy_entry_and_check_status(metadata,
>> self._mount_point):
>> +            return False
>> +
>> +        # This is sync-operation. Thus, call the callback.
>> +        self._post_successful_copy(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, None)
>> +        self._temp_file_path_list = []
>> +
>> +    def _proceed_with_copy(self, metadata):
>> +        if not self._file_path_valid(metadata):
>> +            return False
>>
>>          clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
>>          clipboard.set_with_data([Gtk.TargetEntry.new('text/uri-list', 0,
>> 0)],
>>                                  self.__clipboard_get_func_cb,
>> -                                self.__clipboard_clear_func_cb, None)
>> +                                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_successful_copy(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,
>> +                                  model.get_documents_path())
>> +
>> +    def _proceed_with_copy(self, metadata):
>> +        if not self._file_path_valid(metadata):
>> +            return False
>> +
>> +        if not self._copy_entry_and_check_status(metadata,
>> +
>> model.get_documents_path()):
>> +            return False
>> +
>> +        # This is sync-operation. Call the post-operation now.
>> +        self._post_successful_copy(metadata)
>> +
>>
>>  class FriendsMenu(Gtk.Menu):
>>      __gtype_name__ = 'JournalFriendsMenu'
>> @@ -413,3 +874,92 @@ 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.SignalFlags.RUN_FIRST,
>> +                             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.IconSize.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.IconSize.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.IconSize.MENU))
>> +            journal_menu.connect('volume-error', self.__volume_error_cb)
>> +            menu.append(journal_menu)
>> +            journal_menu.show()
>> +
>> +        volume_monitor = Gio.VolumeMonitor.get()
>> +        icon_theme = Gtk.IconTheme.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.IconSize.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 e1e6331..76b691b 100644
>> --- a/src/jarabe/journal/volumestoolbar.py
>> +++ b/src/jarabe/journal/volumestoolbar.py
>> @@ -193,6 +193,9 @@ class VolumesToolbar(Gtk.Toolbar):
>>      def _set_up_volumes(self):
>>          self._set_up_documents_button()
>>
>> +        client = GConf.Client.get_default()
>> +        color = XoColor(client.get_string('/desktop/sugar/user/color'))
>> +
>>          volume_monitor = Gio.VolumeMonitor.get()
>>          self._mount_added_hid = volume_monitor.connect('mount-added',
>>
>> self.__mount_added_cb)
>> @@ -202,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()
>> @@ -217,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)
>>
>> @@ -247,10 +255,26 @@ 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:
>> +            button.set_active(True)
>> +            from jarabe.journal.journalactivity import get_journal
>> +            journal = get_journal()
>> +
>> +            journal.hide_alert()
>> +            journal.get_list_view()._selected_entries = 0
>> +            journal.switch_to_editing_mode(False)
>> +            journal.get_list_view().inhibit_refresh(False)
>> +
>>              self.emit('volume-changed', button.mount_point)
>>
>> +    def _unmount_activated_cb(self, menu_item, mount):
>> +        logging.debug('VolumesToolbar._unmount_activated_cb: %r', mount)
>> +        mount.unmount(self.__unmount_cb)
>> +
>> +    def __unmount_cb(self, source, result):
>> +        logging.debug('__unmount_cb %r %r', source, result)
>> +
>>      def _get_button_for_mount(self, mount):
>>          mount_point = mount.get_root().get_path()
>>          for button in self.get_children():
>> @@ -259,6 +283,13 @@ class VolumesToolbar(Gtk.Toolbar):
>>          logging.error('Couldnt find button with mount_point %r',
>> mount_point)
>>          return None
>>
>> +    def _get_button_for_mount_point(self, mount_point):
>> +        for button in self.get_children():
>> +            if button.mount_point == mount_point:
>> +                return button
>> +        logging.error('Couldnt find button with mount_point %r',
>> mount_point)
>> +        return None
>> +
>>      def _remove_button(self, mount):
>>          button = self._get_button_for_mount(mount)
>>          self._volume_buttons.remove(button)
>> @@ -272,6 +303,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__ = {
>> @@ -293,21 +330,25 @@ class BaseButton(RadioToolButton):
>>                                 selection_data, info, timestamp):
>>          object_id = selection_data.data
>>          metadata = model.get(object_id)
>> -        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
>>
>> -        try:
>> -            model.copy(metadata, self.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'))
>> +        from jarabe.journal.palettes import CopyMenu,
>> get_copy_menu_helper
>> +        copy_menu_helper = get_copy_menu_helper()
>> +
>> +        dummy_copy_menu = CopyMenu()
>> +        copy_menu_helper.insert_copy_to_menu_items(dummy_copy_menu,
>> +                                                   [metadata],
>> +                                                   False,
>> +                                                   False,
>> +                                                   False)
>> +
>> +        # Now, activate the menuitem, whose mount-point matches the
>> +        # mount-point of the button, upon whom the item has been
>> +        # dragged.
>> +        children_menu_items = dummy_copy_menu.get_children()
>> +        for child in children_menu_items:
>> +            if child.get_mount_point() == self.mount_point:
>> +                child.activate()
>> +                return
>>
>>
>>  class VolumeButton(BaseButton):
>> @@ -386,12 +427,13 @@ 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._mount = dir_path
>>
>> -        self.props.icon_name = 'user-documents'
>> +        self.props.icon_name = icon_name
>>
>>          client = GConf.Client.get_default()
>>          color = XoColor(client.get_string('/desktop/sugar/user/color'))
>> --
>> 1.8.1.2
>>
>>
>
>
> --
> Regards,
>
> Ajay Garg
> Dextrose Developer
> Activity Central: http://activitycentral.com
>



-- 
Daniel Narvaez
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.sugarlabs.org/archive/sugar-devel/attachments/20130414/b8c22138/attachment-0001.html>


More information about the Sugar-devel mailing list