Ticket #3317: 0001-sugar-sl-3317.patch

File 0001-sugar-sl-3317.patch, 50.4 KB (added by ajay_garg, 12 years ago)
  • src/jarabe/journal/journalactivity.py

    From 8733a8cf9fc4a05564a5fc6f328b8f2a077419b1 Mon Sep 17 00:00:00 2001
    From: Ajay Garg <ajay@activitycentral.com>
    Date: Mon, 6 Feb 2012 01:29:12 +0530
    Subject: [sugar PATCH] sl#3317: Batch Operations on Journal Entries (Copy, Erase)
    Organization: Sugar Labs Foundation
    Signed-off-by: Ajay Garg <ajay@activitycentral.com>
    ---
    
    Note that this patch MUST be applied after the applying of patch at :
    http://patchwork.sugarlabs.org/patch/1157/
    
    
    == This code has been written almost exclusively by Martin Abente.
    
    == Design discussions at :
       http://wiki.sugarlabs.org/go/Features/Multi_selection
    
    == Screenshots at :
       http://wiki.sugarlabs.org/go/Features/Multi_selection_screenshots
    
    == Martin's work's video at :
       http://www.sugarlabs.org/~tch/journal2.mpeg
    
    
    
    Following are the changes/enhancements from Martin's work :
    
    a. More copy-to options :: Clipboard, Documents (in addition to mounted drives). 
    
    b. After entries are copied to another location, both - the source and the target - entries
       are de-selected automatically, without the user explicitly have to de-select them all manually. 
    
    c. There has been a progress bar added for batch-operations. 
    
    
    Codewise, effore has been put to have maximum code-reuse; and mimimal code-duplication,
    as most of the operation-parts are same, irrespective of the operation performed.
       
    
     src/jarabe/journal/journalactivity.py |  135 ++++++++-
     src/jarabe/journal/journaltoolbox.py  |  176 ++++++++++-
     src/jarabe/journal/listmodel.py       |   13 +
     src/jarabe/journal/listview.py        |   48 +++
     src/jarabe/journal/model.py           |   13 +-
     src/jarabe/journal/palettes.py        |  574 +++++++++++++++++++++++++++------
     6 files changed, 844 insertions(+), 115 deletions(-)
    
    diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
    index 8cafef0..6f03a73 100644
    a b  
    11# Copyright (C) 2006, Red Hat, Inc.
    22# Copyright (C) 2007, One Laptop Per Child
     3# Copyright (C) 2012, Walter Bender  <walter@sugarlabs.org>
     4# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
     5# Copyright (C) 2012, Martin Abente  <tch@sugarlabs.org>
     6# Copyright (C) 2012, Ajay Garg      <ajay@activitycentral.com>
    37#
    48# This program is free software; you can redistribute it and/or modify
    59# it under the terms of the GNU General Public License as published by
     
    1721
    1822import logging
    1923from gettext import gettext as _
     24from gettext import ngettext
    2025import uuid
    2126
    2227import gtk
    2328import dbus
    2429import statvfs
    2530import os
     31import gobject
    2632
    2733from sugar.graphics.window import Window
    28 from sugar.graphics.alert import ErrorAlert
     34from sugar.graphics.alert import Alert, ErrorAlert
     35from sugar.graphics.icon import Icon
    2936
    3037from sugar.bundle.bundle import ZipExtractException, RegistrationException
    3138from sugar import env
    from sugar import wm 
    3441
    3542from jarabe.model import bundleregistry
    3643from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
     44from jarabe.journal.journaltoolbox import EditToolbox
    3745from jarabe.journal.listview import ListView
     46from jarabe.journal.listmodel import ListModel
    3847from jarabe.journal.detailview import DetailView
    3948from jarabe.journal.volumestoolbar import VolumesToolbar
    4049from jarabe.journal import misc
    _SPACE_TRESHOLD = 52428800 
    5362_BUNDLE_ID = 'org.laptop.JournalActivity'
    5463
    5564_journal = None
     65_mount_point = None
    5666
    5767
    5868class JournalActivityDBusService(dbus.service.Object):
    class JournalActivity(JournalWindow): 
    119129        self._list_view = None
    120130        self._detail_view = None
    121131        self._main_toolbox = None
     132        self._edit_toolbox = None
    122133        self._detail_toolbox = None
    123134        self._volumes_toolbar = None
     135        self._editing_mode = False
     136        self._editing_alert = None
     137        self._info_alert = None
     138        self._selected_entries = []
     139
     140        set_mount_point('/')
    124141
    125142        self._setup_main_view()
    126143        self._setup_secondary_view()
    class JournalActivity(JournalWindow): 
    184201        search_toolbar = self._main_toolbox.search_toolbar
    185202        search_toolbar.connect('query-changed', self._query_changed_cb)
    186203        search_toolbar.set_mount_point('/')
    187         self._mount_point = '/'
     204        set_mount_point('/')
    188205
    189206    def _setup_secondary_view(self):
    190207        self._secondary_view = gtk.VBox()
    class JournalActivity(JournalWindow): 
    217234        self.show_main_view()
    218235
    219236    def show_main_view(self):
    220         if self.toolbar_box != self._main_toolbox:
    221             self.set_toolbar_box(self._main_toolbox)
    222             self._main_toolbox.show()
     237        if self._editing_mode:
     238            toolbox = EditToolbox()
     239        else:
     240            toolbox = self._main_toolbox
     241
     242        self.set_toolbar_box(toolbox)
     243        toolbox.show()
    223244
    224245        if self.canvas != self._main_view:
    225246            self.set_canvas(self._main_view)
    class JournalActivity(JournalWindow): 
    254275    def __volume_changed_cb(self, volume_toolbar, mount_point):
    255276        logging.debug('Selected volume: %r.', mount_point)
    256277        self._main_toolbox.search_toolbar.set_mount_point(mount_point)
    257         self._mount_point = mount_point
     278        set_mount_point(mount_point)
    258279        self._main_toolbox.set_current_toolbar(0)
    259280
    260281    def __model_created_cb(self, sender, **kwargs):
    class JournalActivity(JournalWindow): 
    364385        self.show_main_view()
    365386        self.search_grab_focus()
    366387
    367     def get_mount_point(self):
    368         return self._mount_point
     388    def switch_to_editing_mode(self, switch):
     389        # (re)-switch, only if not already.
     390        if (switch) and (not self._editing_mode):
     391            self._editing_mode = True
     392            self.show_main_view()
     393        elif (not switch) and (self._editing_mode):
     394            self._editing_mode = False
     395            self.show_main_view()
     396
     397    def get_list_view(self):
     398        return self._list_view
     399
     400    def remove_editing_alert(self):
     401        if self._editing_alert is not None:
     402            self.remove_alert(self._editing_alert)
     403        self._editing_alert = None
     404
     405    def add_editing_alert(self, widget_clicked, title, message, operation,
     406            callback):
     407        cancel_icon = Icon(icon_name='dialog-cancel')
     408        ok_icon = Icon(icon_name='dialog-ok')
     409
     410        alert = Alert()
     411        alert.props.title = title
     412        alert.props.msg = message
     413        alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
     414        alert.add_button(gtk.RESPONSE_OK, operation, ok_icon)
     415        alert.connect('response', self.__check_for_action, callback)
     416        alert.show()
     417
     418        self.remove_editing_alert()
     419
     420        self._editing_alert = alert
     421        self.add_alert(alert)
     422
     423    def __check_for_action(self, alert, response_id, callback):
     424        self.remove_editing_alert()
     425        if response_id == gtk.RESPONSE_OK:
     426            gobject.idle_add(callback, None)
     427
     428    def remove_info_alert(self):
     429        if self._info_alert is not None:
     430            self.remove_alert(self._info_alert)
     431            logging.debug('alert removed')
     432        self._info_alert = None
     433
     434    def add_info_alert(self, button, title, message, show_skip_options,
     435                       callback, data):
     436        skip_icon = Icon(icon_name='dialog-cancel')
     437        skip_all_icon = Icon(icon_name='dialog-cancel')
     438
     439        alert = Alert()
     440        alert.props.title = title
     441        alert.props.msg = message
     442
     443        if show_skip_options:
     444            alert.add_button(gtk.RESPONSE_CANCEL, _('OK'), skip_icon)
     445
     446            # Let the user explicitly see each message of the
     447            # non-operatable entry.
     448            #alert.add_button(gtk.RESPONSE_OK, _('Do not notify again'), skip_all_icon)
     449
     450            alert.connect('response', self.__check_for_skip_action,
     451                          callback, data)
     452
     453        alert.show()
     454        self.remove_info_alert()
     455        self._info_alert = alert
     456
     457        if show_skip_options:
     458            self.add_alert(alert)
     459        else:
     460            self.add_alert_and_callback(alert, callback, data)
     461
     462    def __check_for_skip_action(self, alert, response_id, callback,
     463                                metadata):
     464        self.remove_editing_alert()
     465        if response_id == gtk.RESPONSE_OK:
     466            gobject.idle_add(callback, True, metadata)
     467        elif response_id == gtk.RESPONSE_CANCEL:
     468            gobject.idle_add(callback, False, metadata)
     469
     470    def get_metadata_list(self, selected_state):
     471        metadata_list = []
     472
     473        list_view_model = self.get_list_view().get_model()
     474        for index in range(0, len(list_view_model)):
     475            metadata = list_view_model.get_metadata(index)
     476            if metadata.get('selected', '0') == selected_state:
     477                metadata_list.append(metadata)
     478
     479        return metadata_list
    369480
    370481
    371482def get_journal():
    def get_journal(): 
    378489
    379490def start():
    380491    get_journal()
     492
     493
     494def set_mount_point(mount_point):
     495    global _mount_point
     496    _mount_point = mount_point
     497
     498def get_mount_point():
     499    return _mount_point
  • src/jarabe/journal/journaltoolbox.py

    diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
    index 2aa4153..7e1419a 100644
    a b  
    11# Copyright (C) 2007, One Laptop Per Child
    22# Copyright (C) 2009, Walter Bender
     3# Copyright (C) 2012, Walter Bender  <walter@sugarlabs.org>
     4# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
     5# Copyright (C) 2012, Martin Abente  <tch@sugarlabs.org>
     6# Copyright (C) 2012, Ajay Garg      <ajay@activitycentral.com>
    37#
    48# This program is free software; you can redistribute it and/or modify
    59# it under the terms of the GNU General Public License as published by
     
    1620# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
    1721
    1822from gettext import gettext as _
     23from gettext import ngettext
    1924import logging
    2025from datetime import datetime, timedelta
    2126import os
    from sugar import mime 
    4348from jarabe.model import bundleregistry
    4449from jarabe.journal import misc
    4550from jarabe.journal import model
    46 from jarabe.journal.palettes import ClipboardMenu
    47 from jarabe.journal.palettes import VolumeMenu
     51from jarabe.journal import palettes
    4852
    4953
    5054_AUTOSEARCH_TIMEOUT = 1000
    _ACTION_MY_FRIENDS = 1 
    6367_ACTION_MY_CLASS = 2
    6468
    6569
     70COPY_MENU_HELPER = palettes.get_copy_menu_helper()
     71
    6672class MainToolbox(Toolbox):
    6773    def __init__(self):
    6874        Toolbox.__init__(self)
    class EntryToolbar(gtk.Toolbar): 
    527533            menu_item.show()
    528534
    529535
     536class EditToolbox(Toolbox):
     537    def __init__(self):
     538        Toolbox.__init__(self)
     539
     540        self.edit_toolbar = EditToolbar()
     541        self.add_toolbar('', self.edit_toolbar)
     542        self.edit_toolbar.show()
     543
     544
     545class EditToolbar(gtk.Toolbar):
     546    def __init__(self):
     547        gtk.Toolbar.__init__(self)
     548
     549        self.add(SelectNoneButton())
     550        self.add(SelectAllButton())
     551        self.add(gtk.SeparatorToolItem())
     552        self.add(BatchEraseButton())
     553        self.add(BatchCopyButton())
     554
     555        self.show_all()
     556
     557
     558class SelectNoneButton(ToolButton, palettes.ActionItem):
     559    def __init__(self):
     560        ToolButton.__init__(self, 'select-none')
     561        palettes.ActionItem.__init__(self, '', None,
     562                                     show_editing_alert=False,
     563                                     show_progress_info_alert=True,
     564                                     batch_mode=True,
     565                                     need_to_popup_options=False,
     566                                     operate_on_deselected_entries=False,
     567                                     switch_to_normal_mode_after_completion=True)
     568        self.props.tooltip = _('Select none')
     569
     570    def _get_actionable_signal(self):
     571        return 'clicked'
     572
     573    def _get_info_alert_title(self):
     574        return _('Deselecting')
     575
     576    def _operate(self, metadata):
     577        metadata['selected'] = '0'
     578        model.write(metadata, update_mtime=False)
     579
     580        # This is sync-operation. Thus, call the callback.
     581        self._post_operate_per_metadata_per_action(metadata)
     582
     583
     584class SelectAllButton(ToolButton, palettes.ActionItem):
     585    def __init__(self):
     586        ToolButton.__init__(self, 'select-all')
     587        palettes.ActionItem.__init__(self, '', None,
     588                                     show_editing_alert=False,
     589                                     show_progress_info_alert=True,
     590                                     batch_mode=True,
     591                                     need_to_popup_options=False,
     592                                     operate_on_deselected_entries=True,
     593                                     switch_to_normal_mode_after_completion=False)
     594        self.props.tooltip = _('Select all')
     595
     596    def _get_actionable_signal(self):
     597        return 'clicked'
     598
     599    def _get_info_alert_title(self):
     600        return _('Selecting')
     601
     602    def _operate(self, metadata):
     603        metadata['selected'] = '1'
     604
     605        file_path = model.get_file(metadata['uid'])
     606        if not file_path or not os.path.exists(file_path):
     607            logging.warn('Entries without a file cannot be copied.')
     608            error_message =  _('Entries without a file cannot be copied.')
     609            if self._batch_mode:
     610                self._handle_error_alert(error_message, metadata)
     611            else:
     612                self.emit('volume-error', error_message, _('Warning'))
     613            return
     614
     615        model.write(metadata, update_mtime=False)
     616
     617        # This is sync-operation. Thus, call the callback.
     618        self._post_operate_per_metadata_per_action(metadata)
     619
     620
     621class BatchEraseButton(ToolButton, palettes.ActionItem):
     622    def __init__(self):
     623        ToolButton.__init__(self, 'edit-delete')
     624        palettes.ActionItem.__init__(self, '', None,
     625                                     show_editing_alert=True,
     626                                     show_progress_info_alert=True,
     627                                     batch_mode=True,
     628                                     need_to_popup_options=False,
     629                                     operate_on_deselected_entries=False,
     630                                     switch_to_normal_mode_after_completion=True)
     631        self.props.tooltip = _('Erase')
     632
     633    def _get_actionable_signal(self):
     634        return 'clicked'
     635
     636    def _get_editing_alert_title(self):
     637        return _('Erase')
     638
     639    def _get_editing_alert_message(self, entries_len):
     640        return ngettext('Do you want to erase %d entry?',
     641                        'Do you want to erase %d entries?',
     642                         entries_len) % (entries_len)
     643
     644    def _get_editing_alert_operation(self):
     645        return _('Erase')
     646
     647    def _get_info_alert_title(self):
     648        return _('Erasing')
     649
     650    def _operate(self, metadata):
     651        model.delete(metadata['uid'])
     652
     653        # This is sync-operation. Thus, call the callback.
     654        self._post_operate_per_metadata_per_action(metadata)
     655
     656
     657class BatchCopyButton(ToolButton, palettes.ActionItem):
     658    def __init__(self):
     659        ToolButton.__init__(self, 'edit-copy')
     660        palettes.ActionItem.__init__(self, '', None,
     661                                     show_editing_alert=True,
     662                                     show_progress_info_alert=True,
     663                                     batch_mode=True,
     664                                     need_to_popup_options=True,
     665                                     operate_on_deselected_entries=False,
     666                                     switch_to_normal_mode_after_completion=True)
     667
     668        self.props.tooltip = _('Copy')
     669
     670        self._metadata_list = None
     671
     672    def _get_actionable_signal(self):
     673        return 'clicked'
     674
     675    def _fill_and_pop_up_options(self, widget_clicked):
     676        for child in self.props.palette.menu.get_children():
     677            self.props.palette.menu.remove(child)
     678
     679        COPY_MENU_HELPER.insert_copy_to_menu_items(self.props.palette.menu,
     680                                                   None,
     681                                                   show_editing_alert=True,
     682                                                   show_progress_info_alert=True,
     683                                                   batch_mode=True)
     684        self.props.palette.popup(immediate=True, state=1)
     685
     686
     687
     688
     689
     690
     691
     692
     693
     694class EditCopyItem(MenuItem):
     695    __gtype_name__ = 'JournalEditCopyItem'
     696
     697    def __init__(self, icon_name, text_label, mount_path):
     698        MenuItem.__init__(self, icon_name=icon_name, text_label=text_label)
     699        self.mount_path = mount_path
     700        self.mount_info = text_label
     701
    530702class SortingButton(ToolButton):
    531703    __gtype_name__ = 'JournalSortingButton'
    532704
  • src/jarabe/journal/listmodel.py

    diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
    index 417ff61..a07f897 100644
    a b  
    11# Copyright (C) 2009, Tomeu Vizoso
     2# Copyright (C) 2012, Walter Bender  <walter@sugarlabs.org>
     3# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
     4# Copyright (C) 2012, Martin Abente  <tch@sugarlabs.org>
     5# Copyright (C) 2012, Ajay Garg      <ajay@activitycentral.com>
    26#
    37# This program is free software; you can redistribute it and/or modify
    48# it under the terms of the GNU General Public License as published by
    class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 
    5458    COLUMN_BUDDY_1 = 9
    5559    COLUMN_BUDDY_2 = 10
    5660    COLUMN_BUDDY_3 = 11
     61    COLUMN_SELECT = 12
    5762
    5863    _COLUMN_TYPES = {
    5964        COLUMN_UID: str,
    class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 
    6873        COLUMN_BUDDY_1: object,
    6974        COLUMN_BUDDY_3: object,
    7075        COLUMN_BUDDY_2: object,
     76        COLUMN_SELECT: bool,
    7177    }
    7278
    7379    _PAGE_SIZE = 10
    class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 
    198204
    199205            self._cached_row.append(None)
    200206
     207        # If an entry was already selected, switch to editing mode.
     208        if metadata.get('selected', '0') == '1':
     209            from jarabe.journal.journalactivity import get_journal
     210            get_journal().switch_to_editing_mode(True)
     211
     212        self._cached_row.append(metadata.get('selected', '0') == '1')
     213
    201214        return self._cached_row[column]
    202215
    203216    def on_iter_nth_child(self, iterator, n):
  • src/jarabe/journal/listview.py

    diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
    index a0ceccc..2aa85ae 100644
    a b  
    11# Copyright (C) 2009, Tomeu Vizoso
     2# Copyright (C) 2012, Walter Bender  <walter@sugarlabs.org>
     3# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
     4# Copyright (C) 2012, Martin Abente  <tch@sugarlabs.org>
     5# Copyright (C) 2012, Ajay Garg      <ajay@activitycentral.com>
    26#
    37# This program is free software; you can redistribute it and/or modify
    48# it under the terms of the GNU General Public License as published by
    class BaseListView(gtk.Bin): 
    98102        self._title_column = None
    99103        self.sort_column = None
    100104        self._add_columns()
     105        self._inhibit_refresh = False
     106        self._selected_entries = 0
    101107
    102108        self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
    103109                                                [('text/uri-list', 0, 0),
    class BaseListView(gtk.Bin): 
    134140            return object_id.startswith(self._query['mountpoints'][0])
    135141
    136142    def _add_columns(self):
     143        cell_select = gtk.CellRendererToggle()
     144        cell_select.props.indicator_size = style.zoom(26)
     145        cell_select.props.activatable = True
     146        cell_select.connect('toggled', self.__selected_cb)
     147
     148        column = gtk.TreeViewColumn()
     149        column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
     150        column.props.fixed_width = style.GRID_CELL_SIZE
     151        column.pack_start(cell_select)
     152        column.add_attribute(cell_select, "active", ListModel.COLUMN_SELECT)
     153        self.tree_view.append_column(column)
     154
    137155        cell_favorite = CellRendererFavorite(self.tree_view)
    138156        cell_favorite.connect('clicked', self.__favorite_clicked_cb)
    139157
    class BaseListView(gtk.Bin): 
    251269        else:
    252270            cell.props.xo_color = None
    253271
     272    def __selected_cb(self, cell, path):
     273        from jarabe.journal.journalactivity import get_journal
     274        journal = get_journal()
     275
     276        row = self._model[path]
     277        metadata = model.get(row[ListModel.COLUMN_UID])
     278        if metadata.get('selected', '0') == '1':
     279            metadata['selected'] = '0'
     280            self._selected_entries = self._selected_entries - 1
     281            if self._selected_entries < 1:
     282                journal.switch_to_editing_mode(False)
     283        else:
     284            metadata['selected'] = '1'
     285            self._selected_entries = self._selected_entries + 1
     286            if self._selected_entries > 0:
     287                journal.switch_to_editing_mode(True)
     288
     289        model.write(metadata, update_mtime=False)
     290
    254291    def __favorite_clicked_cb(self, cell, path):
    255292        row = self._model[path]
    256293        metadata = model.get(row[ListModel.COLUMN_UID])
    class BaseListView(gtk.Bin): 
    274311                             ListModel.COLUMN_TIMESTAMP))
    275312        self._query = query_dict
    276313
     314        # This refresh is always needed, since the query has changed.
    277315        self.refresh()
    278316
    279317    def refresh(self):
     318        if not self._inhibit_refresh:
     319            self.proceed_with_refresh()
     320
     321    def proceed_with_refresh(self):
    280322        logging.debug('ListView.refresh query %r', self._query)
    281323        self._stop_progress_bar()
    282324
    class BaseListView(gtk.Bin): 
    466508        self.update_dates()
    467509        return True
    468510
     511    def get_model(self):
     512        return self._model
     513
     514    def inhibit_refresh(self, inhibit):
     515        self._inhibit_refresh = inhibit
     516
    469517
    470518class ListView(BaseListView):
    471519    __gtype_name__ = 'JournalListView'
  • src/jarabe/journal/model.py

    diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
    index 5285a7c..39547a4 100644
    a b  
    11# Copyright (C) 2007-2011, One Laptop per Child
     2# Copyright (C) 2012, Walter Bender  <walter@sugarlabs.org>
     3# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
     4# Copyright (C) 2012, Martin Abente  <tch@sugarlabs.org>
     5# Copyright (C) 2012, Ajay Garg      <ajay@activitycentral.com>
    26#
    37# This program is free software; you can redistribute it and/or modify
    48# it under the terms of the GNU General Public License as published by
    from sugar import dispatch 
    3741from sugar import mime
    3842from sugar import util
    3943
    40 
    4144DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
    4245DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
    4346DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
    DS_DBUS_PATH = '/org/laptop/sugar/DataStore' 
    4548# Properties the journal cares about.
    4649PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id',
    4750              'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type',
    48               'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid']
     51              'mountpoint', 'mtime', 'progress', 'timestamp', 'title',
     52              'uid', 'selected']
    4953
    5054MIN_PAGES_TO_CACHE = 3
    5155MAX_PAGES_TO_CACHE = 5
    def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): 
    651655                                                 file_path,
    652656                                                 transfer_ownership)
    653657    else:
     658        # HACK: For documents: modify the mount-point
     659        from jarabe.journal.journalactivity import get_mount_point
     660        if get_mount_point() == get_documents_path():
     661            metadata['mountpoint'] = get_documents_path()
     662
    654663        object_id = _write_entry_on_external_device(metadata, file_path)
    655664
    656665    return object_id
  • src/jarabe/journal/palettes.py

    diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
    index 27b0b54..2916a14 100644
    a b  
    11# Copyright (C) 2008 One Laptop Per Child
     2# Copyright (C) 2012, Walter Bender  <walter@sugarlabs.org>
     3# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
     4# Copyright (C) 2012, Martin Abente  <tch@sugarlabs.org>
     5# Copyright (C) 2012, Ajay Garg      <ajay@activitycentral.com>
    26#
    37# This program is free software; you can redistribute it and/or modify
    48# it under the terms of the GNU General Public License as published by
     
    1519# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
    1620
    1721from gettext import gettext as _
     22from gettext import ngettext
    1823import logging
    1924import os
    2025
    import gtk 
    2328import gconf
    2429import gio
    2530import glib
     31import time
     32
     33from sugar import _sugarext
    2634
    2735from sugar.graphics import style
    2836from sugar.graphics.palette import Palette
    from jarabe.journal import model 
    3947
    4048friends_model = friends.get_model()
    4149
     50_copy_menu_helper = None
     51
    4252
    4353class BulkOperationDetails():
    4454
    class ObjectPalette(Palette): 
    129139        menu_item.set_image(icon)
    130140        self.menu.append(menu_item)
    131141        menu_item.show()
    132         copy_menu = CopyMenu(metadata)
     142        copy_menu = CopyMenu()
     143        copy_menu_helper = get_copy_menu_helper()
     144
     145        metadata_list = []
     146        metadata_list.append(metadata)
     147        copy_menu_helper.insert_copy_to_menu_items(copy_menu,
     148                                                   metadata_list,
     149                                                   False,
     150                                                   False,
     151                                                   False)
    133152        copy_menu.connect('volume-error', self.__volume_error_cb)
     153        copy_menu_helper.connect('volume-error', self.__volume_error_cb)
    134154        menu_item.set_submenu(copy_menu)
    135155
    136156        if self._metadata['mountpoint'] == '/':
    class CopyMenu(gtk.Menu): 
    260280                         ([str, str])),
    261281    }
    262282
    263     def __init__(self, metadata):
     283    def __init__(self):
    264284        gobject.GObject.__init__(self)
    265285
    266         self._metadata = metadata
    267286
    268         clipboard_menu = ClipboardMenu(self._metadata)
    269         clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
    270                                       icon_size=gtk.ICON_SIZE_MENU))
    271         clipboard_menu.connect('volume-error', self.__volume_error_cb)
    272         self.append(clipboard_menu)
    273         clipboard_menu.show()
     287class ActionItem(gobject.GObject):
     288    """
     289    This class implements the course of actions that happens when clicking
     290    upon an Action-Item (eg. Batch-Copy-Toolbar-button;
     291                             Actual-Batch-Copy-To-Journal-button;
     292                             Actual-Batch-Copy-To-Documents-button;
     293                             Actual-Batch-Copy-To-Mounted-Drive-button;
     294                             Actual-Batch-Copy-To-Clipboard-button;
     295                             Single-Copy-To-Journal-button;
     296                             Single-Copy-To-Documents-button;
     297                             Single-Copy-To-Mounted-Drive-button;
     298                             Single-Copy-To-Clipboard-button;
     299                             Batch-Erase-Button;
     300                             Select-None-Toolbar-button;
     301                             Select-All-Toolbar-button
     302    """
    274303
    275         from jarabe.journal import journalactivity
    276         journal_model = journalactivity.get_journal()
    277         if journal_model.get_mount_point() != model.get_documents_path():
    278             documents_menu = DocumentsMenu(self._metadata)
    279             documents_menu.set_image(Icon(icon_name='user-documents',
    280                                           icon_size=gtk.ICON_SIZE_MENU))
    281             documents_menu.connect('volume-error', self.__volume_error_cb)
    282             self.append(documents_menu)
    283             documents_menu.show()
     304    def __init__(self, label, metadata_list, show_editing_alert,
     305                 show_progress_info_alert, batch_mode,
     306                 need_to_popup_options,
     307                 operate_on_deselected_entries,
     308                 switch_to_normal_mode_after_completion):
     309        gobject.GObject.__init__(self)
    284310
    285         if self._metadata['mountpoint'] != '/':
    286             client = gconf.client_get_default()
    287             color = XoColor(client.get_string('/desktop/sugar/user/color'))
    288             journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
    289             journal_menu.set_image(Icon(icon_name='activity-journal',
    290                                         xo_color=color,
    291                                         icon_size=gtk.ICON_SIZE_MENU))
    292             journal_menu.connect('volume-error', self.__volume_error_cb)
    293             self.append(journal_menu)
    294             journal_menu.show()
     311        self._label = label
     312        self._metadata_list = metadata_list
     313        self._show_progress_info_alert = show_progress_info_alert
     314        self._batch_mode = batch_mode
     315        self._operate_on_deselected_entries = \
     316                operate_on_deselected_entries
     317        self._switch_to_normal_mode_after_completion = \
     318                switch_to_normal_mode_after_completion
    295319
    296         volume_monitor = gio.volume_monitor_get()
    297         icon_theme = gtk.icon_theme_get_default()
    298         for mount in volume_monitor.get_mounts():
    299             if self._metadata['mountpoint'] == mount.get_root().get_path():
    300                 continue
    301             volume_menu = VolumeMenu(self._metadata, mount.get_name(),
    302                                    mount.get_root().get_path())
    303             for name in mount.get_icon().props.names:
    304                 if icon_theme.has_icon(name):
    305                     volume_menu.set_image(Icon(icon_name=name,
    306                                                icon_size=gtk.ICON_SIZE_MENU))
    307                     break
    308             volume_menu.connect('volume-error', self.__volume_error_cb)
    309             self.append(volume_menu)
    310             volume_menu.show()
     320        actionable_signal = self._get_actionable_signal()
    311321
    312     def __volume_error_cb(self, menu_item, message, severity):
    313         self.emit('volume-error', message, severity)
     322        if need_to_popup_options:
     323            self.connect(actionable_signal, self._fill_and_pop_up_options)
     324        else:
     325            if show_editing_alert:
     326                self.connect(actionable_signal, self._show_editing_alert)
     327            else:
     328                self.connect(actionable_signal, self._pre_operate_per_action)
     329
     330    def _get_actionable_signal(self):
     331        """
     332        Some widgets like 'buttons' have 'clicked' as actionable signal;
     333        some like 'menuitems' have 'activate' as actionable signal.
     334        """
     335
     336        raise NotImplementedError
     337
     338    def _fill_and_pop_up_options(self):
     339        """
     340        Eg. Batch-Copy-Toolbar-button does not do anything by itself
     341        useful; but rather pops-up the actual 'copy-to' options.
     342        """
     343
     344        raise NotImplementedError
     345
     346    def _show_editing_alert(self, widget_clicked):
     347        """
     348        Upon clicking the actual operation button (eg.
     349        Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT
     350        Batch-Copy-Toolbar-button, since it does not do anything
     351        actually useful, but only pops-up the actual 'copy-to' options.
     352        """
     353
     354        alert_parameters = self._get_editing_alert_parameters()
     355        title = alert_parameters[0]
     356        message = alert_parameters[1]
     357        operation = alert_parameters[2]
     358
     359        from jarabe.journal.journalactivity import get_journal
     360        get_journal().add_editing_alert(None, title, message, operation,
     361                                        self._pre_operate_per_action)
     362
     363    def _get_editing_alert_parameters(self):
     364        """
     365        Get the alert parameters for widgets that can show editing
     366        alert.
     367        """
     368
     369        # For batch-operations, fetch the metadata-list.
     370        if self._batch_mode:
     371            self._metadata_list = self._get_metadata_list()
     372        entries_len = len(self._metadata_list)
     373
     374        title = self._get_editing_alert_title()
     375        message = self._get_editing_alert_message(entries_len)
     376        operation = self._get_editing_alert_operation()
     377
     378        return (title, message, operation)
     379
     380    def _get_metadata_list(self):
     381        """
     382        Get the metadata list, according to button-type. For eg,
     383        Select-All-Toolbar-button operates on non-selected entries;
     384        while othere operate on selected-entries.
     385        """
     386
     387        from jarabe.journal.journalactivity import get_journal
     388        journal = get_journal()
     389
     390        if self._operate_on_deselected_entries:
     391            return journal.get_metadata_list('0')
     392        else:
     393            return journal.get_metadata_list('1')
     394
     395    def _get_editing_alert_title(self):
     396        raise NotImplementedError
     397
     398    def _get_editing_alert_message(self, entries_len):
     399        raise NotImplementedError
     400
     401    def _get_editing_alert_operation(self):
     402        raise NotImplementedError
     403
     404    def _is_metadata_list_empty(self):
     405        return (self._metadata_list is None) or \
     406                (len(self._metadata_list) == 0)
     407
     408    def _pre_operate_per_action(self, obj):
     409        """
     410        This is the stage, just before the FIRST metadata gets into its
     411        processing cycle.
     412        """
     413
     414        self._skip_all = False
     415
     416        # For batch-operations, fetch the metadata list again.
     417        if (self._batch_mode):
     418            self._metadata_list = self._get_metadata_list()
     419
     420        # Set the initial length of metadata-list.
     421        self._metadata_list_initial_len = len(self._metadata_list)
     422
     423        # Next, proceed with the metadata
     424        self._pre_operate_per_metadata_per_action()
     425
     426    def _pre_operate_per_metadata_per_action(self):
     427        """
     428        This is the stage, just before EVERY metadata gets into doing
     429        its actual work.
     430        """
     431
     432        # If there is still some metadata left, proceed with the
     433        # metadata operation.
     434        # Else, proceed to post-operations.
     435        if len(self._metadata_list) > 0:
     436            metadata = self._metadata_list.pop(0)
     437
     438            # De-select the entry.
     439            metadata['selected'] = '0'
     440            model.write(metadata, update_mtime=False)
     441
     442            # If info-alert needs to be shown, show the alert, and
     443            # arrange for actual operation.
     444            # Else, proceed to actual operation directly.
     445            if self._show_progress_info_alert:
     446                current_len = len(self._metadata_list)
     447
     448                # TRANS: Do not translate the two %d, and the %s.
     449                info_alert_message = _('( %d / %d ) %s') % (
     450                        self._metadata_list_initial_len - current_len,
     451                        self._metadata_list_initial_len, metadata['title'])
     452
     453                from jarabe.journal.journalactivity import get_journal
     454                get_journal().add_info_alert(None,
     455                                             self._get_info_alert_title() + ' ...',
     456                                             info_alert_message, False,
     457                                             self._operate_per_metadata_per_action,
     458                                             metadata)
     459            else:
     460                self._operate_per_metadata_per_action(metadata)
     461        else:
     462            self._post_operate_per_action()
     463
     464    def _get_info_alert_title(self):
     465        raise NotImplementedError
     466
     467    def _operate_per_metadata_per_action(self, metadata):
     468        """
     469        This is just a code-convenient-function, which allows
     470        runtime-overriding. It just delegates to the actual
     471        "self._operate" method, the actual which is determined at
     472        runtime.
     473        """
     474
     475        # Pass the callback for the post-operation-for-metadata. This
     476        # will ensure that async-operations on the metadata are taken
     477        # care of.
     478        self._operate(metadata)
     479
     480    def _operate(self, metadata):
     481        """
     482        Actual, core, productive stage for EVERY metadata.
     483        """
     484
     485        raise NotImplementedError
     486
     487    def  _post_operate_per_metadata_per_action(self, metadata):
     488        """
     489        This is the stage, just after EVERY metadata has been
     490        processed.
     491        """
     492
     493        from jarabe.journal.journalactivity import get_journal
     494        get_journal().remove_info_alert()
     495
     496        # Call the next ...
     497        self._pre_operate_per_metadata_per_action()
     498
     499    def _post_operate_per_action(self):
     500        """
     501        This is the stage, just after the LAST metadata has been
     502        processed.
     503        """
     504
     505        # Switch to non-editing mode (if applicable), after the operation is complete
     506        if self._switch_to_normal_mode_after_completion:
     507            from jarabe.journal.journalactivity import get_journal
     508            get_journal().switch_to_editing_mode(False)
     509
     510    def _inhibit_refresh(self, inhibit):
     511        from jarabe.journal.journalactivity import get_journal
     512        get_journal().get_list_view().inhibit_refresh(inhibit)
     513
     514    def _refresh(self):
     515        from jarabe.journal.journalactivity import get_journal
     516        get_journal().get_list_view().refresh()
     517
     518    def _handle_error_alert(self, error_message, metadata):
     519        """
     520        This handles any error scenarios. Examples are of entries that
     521        display the message "Entries without a file cannot be copied."
     522        This is kind of controller-functionl the model-function is
     523        "self._set_error_info_alert".
     524        """
     525
     526        if self._skip_all:
     527            self._post_operate_per_metadata_per_action(metadata)
     528        else:
     529            self._set_error_info_alert(error_message, metadata)
     530
     531    def _set_error_info_alert(self, error_message, metadata):
     532        """
     533        This method displays the error alert.
     534        """
     535
     536        current_len = len(self._metadata_list)
    314537
     538        # TRANS: Do not translate the two %d, and the three %s.
     539        info_alert_message = _('( %d / %d ) Error while %s %s : %s') % (
     540                self._metadata_list_initial_len - current_len,
     541                self._metadata_list_initial_len,
     542                self._get_info_alert_title(),
     543                metadata['title'],
     544                error_message)
    315545
    316 class VolumeMenu(MenuItem):
    317     __gtype_name__ = 'JournalVolumeMenu'
     546        from jarabe.journal.journalactivity import get_journal
     547        get_journal().add_info_alert(None,
     548                                     self._get_info_alert_title()  + ' ...',
     549                                     info_alert_message, True,
     550                                     self._process_error_skipping,
     551                                     metadata)
    318552
     553    def _process_error_skipping(self, skip_all, metadata):
     554        if skip_all:
     555            self._skip_all = True
     556
     557        # The operation for the current metadata is finished (kinda
     558        # pseudo ...)
     559        self._post_operate_per_metadata_per_action(metadata)
     560
     561
     562class BaseCopyMenuItem(MenuItem, ActionItem):
    319563    __gsignals__ = {
    320         'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
    321                          ([str, str])),
    322     }
     564            'volume-error': (gobject.SIGNAL_RUN_FIRST,
     565                             gobject.TYPE_NONE, ([str, str])),
     566            }
    323567
    324     def __init__(self, metadata, label, mount_point):
     568
     569    def __init__(self, metadata_list, label, show_editing_alert,
     570                 show_progress_info_alert, batch_mode):
    325571        MenuItem.__init__(self, label)
    326         self._metadata = metadata
    327         self.connect('activate', self.__copy_to_volume_cb, mount_point)
     572        ActionItem.__init__(self, label, metadata_list, show_editing_alert,
     573                            show_progress_info_alert, batch_mode,
     574                            need_to_popup_options=False,
     575                            operate_on_deselected_entries=False,
     576                            switch_to_normal_mode_after_completion=True)
    328577
    329     def __copy_to_volume_cb(self, menu_item, mount_point):
    330         file_path = model.get_file(self._metadata['uid'])
     578    def _get_actionable_signal(self):
     579        return 'activate'
     580
     581    def _get_editing_alert_title(self):
     582        return _('Copy')
     583
     584    def _get_editing_alert_message(self, entries_len):
     585        return ngettext('Do you want to copy %d entry to %s?',
     586                        'Do you want to copy %d entries to %s?',
     587                        entries_len) % (entries_len, self._label)
     588
     589    def _get_editing_alert_operation(self):
     590        return _('Copy')
     591
     592    def _get_info_alert_title(self):
     593        return _('Copying')
     594
     595
     596class VolumeMenu(BaseCopyMenuItem):
     597    def __init__(self, metadata_list, label, mount_point,
     598                 show_editing_alert, show_progress_info_alert,
     599                 batch_mode):
     600        BaseCopyMenuItem.__init__(self, metadata_list, label,
     601                                  show_editing_alert,
     602                                  show_progress_info_alert, batch_mode)
     603        self._mount_point = mount_point
     604
     605    def _operate(self, metadata):
     606        file_path = model.get_file(metadata['uid'])
    331607
    332608        if not file_path or not os.path.exists(file_path):
    333609            logging.warn('Entries without a file cannot be copied.')
    334             self.emit('volume-error',
    335                       _('Entries without a file cannot be copied.'),
    336                       _('Warning'))
     610            error_message =  _('Entries without a file cannot be copied.')
     611            if self._batch_mode:
     612                self._handle_error_alert(error_message, metadata)
     613            else:
     614                self.emit('volume-error', error_message, _('Warning'))
    337615            return
    338616
    339617        try:
    340             model.copy(self._metadata, mount_point)
     618            model.copy(metadata, self._mount_point)
    341619        except IOError, e:
    342620            logging.exception('Error while copying the entry. %s', e.strerror)
    343             self.emit('volume-error',
    344                       _('Error while copying the entry. %s') % e.strerror,
    345                       _('Error'))
    346 
    347 
    348 class ClipboardMenu(MenuItem):
    349     __gtype_name__ = 'JournalClipboardMenu'
     621            error_message = _('Error while copying the entry. %s') % e.strerror
     622            if self._batch_mode:
     623                self._handle_error_alert(error_message, metadata)
     624            else:
     625                self.emit('volume-error', error_message, _('Error'))
     626            return
    350627
    351     __gsignals__ = {
    352         'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
    353                          ([str, str])),
    354     }
     628        # This is sync-operation. Thus, call the callback.
     629        self._post_operate_per_metadata_per_action(metadata)
    355630
    356     def __init__(self, metadata):
    357         MenuItem.__init__(self, _('Clipboard'))
    358631
    359         self._temp_file_path = None
    360         self._metadata = metadata
    361         self.connect('activate', self.__copy_to_clipboard_cb)
     632class ClipboardMenu(BaseCopyMenuItem):
     633    def __init__(self, metadata_list, show_editing_alert,
     634                 show_progress_info_alert, batch_mode):
     635        BaseCopyMenuItem.__init__(self, metadata_list, _('Clipboard'),
     636                                  show_editing_alert,
     637                                  show_progress_info_alert,
     638                                  batch_mode)
     639        self._temp_file_path_list = []
    362640
    363     def __copy_to_clipboard_cb(self, menu_item):
    364         file_path = model.get_file(self._metadata['uid'])
     641    def _operate(self, metadata):
     642        file_path = model.get_file(metadata['uid'])
    365643        if not file_path or not os.path.exists(file_path):
    366644            logging.warn('Entries without a file cannot be copied.')
    367             self.emit('volume-error',
    368                       _('Entries without a file cannot be copied.'),
    369                       _('Warning'))
     645            error_message = _('Entries without a file cannot be copied.')
     646            if self._batch_mode:
     647                self._handle_error_alert(error_message, metadata)
     648            else:
     649                self.emit('volume-error', error_message, _('Warning'))
    370650            return
    371651
    372652        clipboard = gtk.Clipboard()
    373653        clipboard.set_with_data([('text/uri-list', 0, 0)],
    374654                                self.__clipboard_get_func_cb,
    375                                 self.__clipboard_clear_func_cb)
     655                                self.__clipboard_clear_func_cb,
     656                                metadata)
    376657
    377     def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
     658    def __clipboard_get_func_cb(self, clipboard, selection_data, info,
     659                                metadata):
    378660        # Get hold of a reference so the temp file doesn't get deleted
    379         self._temp_file_path = model.get_file(self._metadata['uid'])
     661        self._temp_file_path = model.get_file(metadata['uid'])
    380662        logging.debug('__clipboard_get_func_cb %r', self._temp_file_path)
    381663        selection_data.set_uris(['file://' + self._temp_file_path])
    382664
    383     def __clipboard_clear_func_cb(self, clipboard, data):
     665    def __clipboard_clear_func_cb(self, clipboard, metadata):
    384666        # Release and delete the temp file
    385667        self._temp_file_path = None
    386668
     669        # This is async-operation; and this is the ending point.
     670        self._post_operate_per_metadata_per_action(metadata)
    387671
    388 class DocumentsMenu(MenuItem):
    389     __gtype_name__ = 'JournalDocumentsMenu'
    390 
    391     __gsignals__ = {
    392         'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
    393                          ([str, str])),
    394     }
    395 
    396     def __init__(self, metadata):
    397         MenuItem.__init__(self, _('Documents'))
    398672
    399         self._temp_file_path = None
    400         self._metadata = metadata
    401         self.connect('activate', self.__copy_to_documents_cb)
     673class DocumentsMenu(BaseCopyMenuItem):
     674    def __init__(self, metadata_list, show_editing_alert,
     675                 show_progress_info_alert, batch_mode):
     676        BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'),
     677                                  show_editing_alert,
     678                                  show_progress_info_alert,
     679                                  batch_mode)
    402680
    403     def __copy_to_documents_cb(self, menu_item):
    404         file_path = model.get_file(self._metadata['uid'])
     681    def _operate(self, metadata):
     682        file_path = model.get_file(metadata['uid'])
    405683        if not file_path or not os.path.exists(file_path):
    406684            logging.warn('Entries without a file cannot be copied.')
    407             self.emit('volume-error',
    408                       _('Entries without a file cannot be copied.'),
    409                       _('Warning'))
     685            error_message = _('Entries without a file cannot be copied.')
     686            if self._batch_mode:
     687                self._handle_error_alert(error_message, metadata)
     688            else:
     689                self.emit('volume-error', error_message, _('Warning'))
    410690            return
    411691
    412         model.copy(self._metadata, model.get_documents_path())
     692        model.copy(metadata, model.get_documents_path())
     693
     694        # This is sync-operation. Call the post-operation now.
     695        self._post_operate_per_metadata_per_action(metadata)
    413696
    414697
    415698class GroupsMenu(gtk.Menu):
    class BuddyPalette(Palette): 
    538821                         icon=buddy_icon)
    539822
    540823        # TODO: Support actions on buddies, like make friend, invite, etc.
     824
     825
     826
     827class CopyMenuHelper(gtk.Menu):
     828    __gtype_name__ = 'JournalCopyMenuHelper'
     829
     830    __gsignals__ = {
     831            'volume-error': (gobject.SIGNAL_RUN_FIRST,
     832                             gobject.TYPE_NONE,
     833                             ([str, str])),
     834            }
     835
     836    def insert_copy_to_menu_items(self, menu, metadata_list,
     837                                  show_editing_alert,
     838                                  show_progress_info_alert,
     839                                  batch_mode):
     840        self._metadata_list = metadata_list
     841
     842        clipboard_menu = ClipboardMenu(metadata_list,
     843                                       show_editing_alert,
     844                                       show_progress_info_alert,
     845                                       batch_mode)
     846        clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
     847                                      icon_size=gtk.ICON_SIZE_MENU))
     848        clipboard_menu.connect('volume-error', self.__volume_error_cb)
     849        menu.append(clipboard_menu)
     850        clipboard_menu.show()
     851
     852        from jarabe.journal.journalactivity import get_mount_point
     853
     854        if get_mount_point() != model.get_documents_path():
     855            documents_menu = DocumentsMenu(metadata_list,
     856                                           show_editing_alert,
     857                                           show_progress_info_alert,
     858                                           batch_mode)
     859            documents_menu.set_image(Icon(icon_name='user-documents',
     860                                          icon_size=gtk.ICON_SIZE_MENU))
     861            documents_menu.connect('volume-error', self.__volume_error_cb)
     862            menu.append(documents_menu)
     863            documents_menu.show()
     864
     865        if get_mount_point() != '/':
     866            client = gconf.client_get_default()
     867            color = XoColor(client.get_string('/desktop/sugar/user/color'))
     868            journal_menu = VolumeMenu(metadata_list, _('Journal'), '/',
     869                                      show_editing_alert,
     870                                      show_progress_info_alert,
     871                                      batch_mode)
     872            journal_menu.set_image(Icon(icon_name='activity-journal',
     873                                        xo_color=color,
     874                                        icon_size=gtk.ICON_SIZE_MENU))
     875            journal_menu.connect('volume-error', self.__volume_error_cb)
     876            menu.append(journal_menu)
     877            journal_menu.show()
     878
     879        volume_monitor = gio.volume_monitor_get()
     880        icon_theme = gtk.icon_theme_get_default()
     881        for mount in volume_monitor.get_mounts():
     882            if get_mount_point() == mount.get_root().get_path():
     883                continue
     884
     885            volume_menu = VolumeMenu(metadata_list, mount.get_name(),
     886                                     mount.get_root().get_path(),
     887                                     show_editing_alert,
     888                                     show_progress_info_alert,
     889                                     batch_mode)
     890            for name in mount.get_icon().props.names:
     891                if icon_theme.has_icon(name):
     892                    volume_menu.set_image(Icon(icon_name=name,
     893                                               icon_size=gtk.ICON_SIZE_MENU))
     894                    break
     895
     896            volume_menu.connect('volume-error', self.__volume_error_cb)
     897            menu.insert(volume_menu, -1)
     898            volume_menu.show()
     899
     900    def __volume_error_cb(self, menu_item, message, severity):
     901        self.emit('volume-error', message, severity)
     902
     903
     904def get_copy_menu_helper():
     905    global _copy_menu_helper
     906    if _copy_menu_helper is None:
     907        _copy_menu_helper = CopyMenuHelper()
     908    return _copy_menu_helper