[Sugar-devel] Faster reviews (was Re: [sugar PATCH] Multi-Select.)

Daniel Narvaez dwnarvaez at gmail.com
Mon Apr 15 03:56:36 EDT 2013


Hi Ajay,

thanks. If you want your patches to be reviewed I think your best chance is
to help out with other people reviews. So far only me, Manuel and Martin
has approved any patch. Until more people participate, reviews are not
going to get any faster...

On 15 April 2013 09:12, Ajay Garg <ajay at activitycentral.com> wrote:

> Thanks Daniel.
>
> I have rebased the patches to the current state of the master branches,
> and created pull requests as per ::
>
> [sugar]             :::  https://github.com/sugarlabs/sugar/pull/22
> [sugar-artwork]  :::  https://github.com/sugarlabs/sugar-artwork/pull/1
>
>
> Rebasing the patches (the "sugar" one) time  and again is a tedious task.
>
> So if this works, please, please, please, please merge the patches into
> the mainline/master as soon as possible.
> If I should do that, please let me know :)
>
> In either case, of course it is me who takes the responsibility of fixing
> any regressions (if at all) that might be introduced.
>
>
> Thanks, and looking forward to a definite action being taken soon, so as
> to avoid a further rebasing of the "sugar" patch.
>
>
>
> On Mon, Apr 15, 2013 at 12:53 AM, Daniel Narvaez <dwnarvaez at gmail.com>wrote:
>
>> 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
>>
>
>
>
> --
> 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/20130415/2295920c/attachment-0001.html>


More information about the Sugar-devel mailing list