[Sugar-devel] [PATCH 82/82] uy#1242: Batch Operations on Journal Entries (Copy, Erase)
Ajay Garg
ajay at activitycentral.com
Tue Feb 14 07:43:23 EST 2012
Note that this patch must be appled in full, and not over version-1, version-2,
version-3, version-4 or version-5 patch.
Also note that this patch MUST be applied after the applying of patch at :
http://patchwork.sugarlabs.org/patch/1157/
== This code has been written almost exclusively by Martin Abente.
== Design discussions at :
http://wiki.sugarlabs.org/go/Features/Multi_selection
== Screenshots at :
http://wiki.sugarlabs.org/go/Features/Multi_selection_screenshots
== Martin's work's video at :
http://www.sugarlabs.org/~tch/journal2.mpeg
Following are the changes/enhancements from Martin's work :
a. More copy-to options :: Clipboard, Documents (in addition to mounted drives).
b. After entries are copied to another location, both - the source and the target - entries
are de-selected automatically, without the user explicitly have to de-select them all manually.
c. There has been a progress bar added for batch-operations.
Codewise, effore has been put to have maximum code-reuse; and mimimal code-duplication,
as most of the operation-parts are same, irrespective of the operation performed.
=================================
CHANGELOG
=================================
Changes of version-2 over version-1
-----------------------------------
Fixed the following :
a. Let's say, there is a saved instance of "Ruler" activity in the
journal, among other possible entries. ("Ruler" is not a special one.
In fact, any activity which in single-mode would display "Entries
without a file cannot be copied" would work).
b. Do "Select All".
c. When "Ruler" gets selected, the message "Error: Entries without a
file cannot be copied" is displayed (at the select-all stage).
EXPECTED BEHAVIOUR ::
a. This message should not be displayed at the time of selection.
Rather, it should be displayed at the time of "Copy-all" operation.
======================================================================
Changes of version-3 over version-2
-----------------------------------
Solved miscellaneous issues (courtesy Anish and Daniel).
A. Sometimes, after doing select-none, the toolbar did not change back
to the normal, non-editing mode.
REASONS
--------
a. The count of selected-entries were being updated, only if the checkboxes were toggled
individually, but not during batch-operations.
b. Also somewhat related, if the user changed to alternate volume-buttons, the
edittoolbox would not be updated everytime. Now, on second thoughts, it seems that
this switching itself should not be allowed, as random switching to different
volumes (with some selected entries still "un-operated") will only make things messy.
FIXES:
------
a. Corrected the count of checkbox changes, both during single-mode and batch-mode.
b. As long as there are some (at least one) selected entries on a particular volume,
rest of the volumetoolbar buttons (in the bottom panel) will be made de-sensitive
(that is, no switching to any other volume will be allowed, until all entries on the
current volume are de-selected, either via indivual deselect; or batch-select; or
operation-completion).
--------------------------------------------------------------------------
B. Now, the status info-alert (of the entries selected/deselected) is shown AFTER
the select-all, and select-none operations have finished.
---------------------------------------------------------------------------
C. Re-coded alerts. Now, there is only alert each (for the singleton journal activity instance)
of the following types ::
(i) Info-alert, which shows the running status; the new status text is simply replaced
(earlier a whole new alert was being generated, resulting in flickering).
(ii) Error-alert, which is "info-alert" plus a "OK" button. Here too, the text is simply
updated, and the corresponding callback called upon clicking "OK" button.
(iii) Confirmation-alert, which is "Error-alert" plus a "Cancel" button.
------------------------------------------------------------------------------
D. Adding clock-cursor during batch-operations.
-------------------------------------------------------------------------------
======================================================================
Changes of version-4 over version-3
-----------------------------------
A. Fixed the regression - "Copy-to" options not available in "View Details" of
a journal entry.
Thanks Anish for catching that.
B. Fixed the regression - "Duplicate" not available in "View Details" of a
journal entry.
Thanks Anish for catching that.
C. Speeded-up batch operations. This has been achieved by inhibiting journal-refresh
during each metadata-processing in batch operations.
The fix is; now, at the start of the batch operation, refresh is inhibited. After the
batch-operation is completed, refresh is allowed back. This results in only one-refresh-
per-action, rather than one-refresh-per-metadata-per-action.
Thanks Daniel for providing me the motivation.
=======================================================================
Changes of version-5 over version-4
-----------------------------------
A. Temporary workaround for the dbus-timeout issue. Enclosed
metadata-copy, and metadata-write operations in a Try/Catch block,
so that ANY exceptions/errors are caught; while the operation
continues to proceeed.
Thanks Anish for catching that.
=======================================================================
Changes of version-6 over version-5
-----------------------------------
A. Any journal-entries with ".xo" extension, were being installed
even though it was only being selected/deselected (in both
single- and double- mode). Fixed it.
Thanks Anish and Sascha.
TODO: Currently, the clock-cursor is running for select-all and
select-none, only when the running-progress is shown for them.
Need to make it work, without the running-progress :-|
src/jarabe/journal/journalactivity.py | 135 ++++++-
src/jarabe/journal/journaltoolbox.py | 243 ++++++++++--
src/jarabe/journal/listmodel.py | 8 +
src/jarabe/journal/listview.py | 58 +++
src/jarabe/journal/model.py | 13 +-
src/jarabe/journal/palettes.py | 719 +++++++++++++++++++++++++++------
src/jarabe/journal/volumestoolbar.py | 9 +
7 files changed, 1023 insertions(+), 162 deletions(-)
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index 8cafef0..e742a18 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -1,5 +1,9 @@
# Copyright (C) 2006, Red Hat, Inc.
# Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2012, Walter Bender <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay at activitycentral.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,15 +21,18 @@
import logging
from gettext import gettext as _
+from gettext import ngettext
import uuid
import gtk
import dbus
import statvfs
import os
+import gobject
from sugar.graphics.window import Window
-from sugar.graphics.alert import ErrorAlert
+from sugar.graphics.alert import Alert, ErrorAlert, ConfirmationAlert
+from sugar.graphics.icon import Icon
from sugar.bundle.bundle import ZipExtractException, RegistrationException
from sugar import env
@@ -34,7 +41,9 @@ from sugar import wm
from jarabe.model import bundleregistry
from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.journaltoolbox import EditToolbox
from jarabe.journal.listview import ListView
+from jarabe.journal.listmodel import ListModel
from jarabe.journal.detailview import DetailView
from jarabe.journal.volumestoolbar import VolumesToolbar
from jarabe.journal import misc
@@ -53,6 +62,7 @@ _SPACE_TRESHOLD = 52428800
_BUNDLE_ID = 'org.laptop.JournalActivity'
_journal = None
+_mount_point = None
class JournalActivityDBusService(dbus.service.Object):
@@ -119,8 +129,21 @@ class JournalActivity(JournalWindow):
self._list_view = None
self._detail_view = None
self._main_toolbox = None
+ self._edit_toolbox = None
self._detail_toolbox = None
self._volumes_toolbar = None
+ self._editing_mode = False
+ self._alert = Alert()
+ self._error_alert = ErrorAlert()
+ self._confirmation_alert = ConfirmationAlert()
+ self._current_alert = None
+ self.setup_handlers_for_alert_actions()
+
+ self._info_alert = None
+ self._selected_entries = []
+ self._bundle_installation_allowed = True
+
+ set_mount_point('/')
self._setup_main_view()
self._setup_secondary_view()
@@ -151,6 +174,9 @@ class JournalActivity(JournalWindow):
self.add_alert(alert)
alert.show()
+ def _volume_error_cb(self, gobject, message, severity):
+ self.update_error_alert(severity, message, None, None)
+
def __alert_response_cb(self, alert, response_id):
self.remove_alert(alert)
@@ -184,7 +210,7 @@ class JournalActivity(JournalWindow):
search_toolbar = self._main_toolbox.search_toolbar
search_toolbar.connect('query-changed', self._query_changed_cb)
search_toolbar.set_mount_point('/')
- self._mount_point = '/'
+ set_mount_point('/')
def _setup_secondary_view(self):
self._secondary_view = gtk.VBox()
@@ -217,9 +243,13 @@ class JournalActivity(JournalWindow):
self.show_main_view()
def show_main_view(self):
- if self.toolbar_box != self._main_toolbox:
- self.set_toolbar_box(self._main_toolbox)
- self._main_toolbox.show()
+ if self._editing_mode:
+ toolbox = EditToolbox()
+ else:
+ toolbox = self._main_toolbox
+
+ self.set_toolbar_box(toolbox)
+ toolbox.show()
if self.canvas != self._main_view:
self.set_canvas(self._main_view)
@@ -254,7 +284,7 @@ class JournalActivity(JournalWindow):
def __volume_changed_cb(self, volume_toolbar, mount_point):
logging.debug('Selected volume: %r.', mount_point)
self._main_toolbox.search_toolbar.set_mount_point(mount_point)
- self._mount_point = mount_point
+ set_mount_point(mount_point)
self._main_toolbox.set_current_toolbar(0)
def __model_created_cb(self, sender, **kwargs):
@@ -281,6 +311,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)
@@ -316,6 +349,9 @@ class JournalActivity(JournalWindow):
metadata['bundle_id'] = bundle.get_bundle_id()
model.write(metadata)
+ def set_bundle_installation_allowed(self, allowed):
+ self._bundle_installation_allowed = allowed
+
def search_grab_focus(self):
search_toolbar = self._main_toolbox.search_toolbar
search_toolbar.give_entry_focus()
@@ -364,8 +400,83 @@ class JournalActivity(JournalWindow):
self.show_main_view()
self.search_grab_focus()
- def get_mount_point(self):
- return self._mount_point
+ def switch_to_editing_mode(self, switch):
+ # Toggle sensitivity of volume-toolbar buttons.
+ self._volumes_toolbar.set_volume_buttons_sensitive(not switch,
+ get_mount_point())
+
+ # (re)-switch, only if not already.
+ if (switch) and (not self._editing_mode):
+ self._editing_mode = True
+ self.show_main_view()
+ elif (not switch) and (self._editing_mode):
+ self._editing_mode = False
+ self.show_main_view()
+
+ def get_list_view(self):
+ return self._list_view
+
+ def setup_handlers_for_alert_actions(self):
+ self._error_alert.connect('response',
+ self.__check_for_alert_action)
+ self._confirmation_alert.connect('response',
+ self.__check_for_alert_action)
+
+ def __check_for_alert_action(self, alert, response_id):
+ self.hide_alert()
+ if self._callback is not None:
+ if response_id == gtk.RESPONSE_OK:
+ gobject.idle_add(self._callback, self._data, True)
+
+ def update_title_and_message(self, alert, title, message):
+ alert.props.title = title
+ alert.props.msg = message
+
+ def update_alert(self, alert):
+ if self._current_alert is None:
+ self.add_alert(alert)
+ elif self._current_alert != alert:
+ self.remove_alert(self._current_alert)
+ self.add_alert(alert)
+
+ self._current_alert = alert
+ self._current_alert.show()
+
+ def hide_alert(self):
+ if self._current_alert is not None:
+ self._current_alert.hide()
+
+ def update_info_alert(self, title, message, callback, data):
+ self.update_title_and_message(self._alert, title, message)
+ self.update_alert(self._alert)
+ if callback is not None:
+ gobject.idle_add(callback, data)
+
+ def update_error_alert(self, title, message, callback, data):
+ self.update_title_and_message(self._error_alert, title,
+ message)
+ self._callback = callback
+ self._data = data
+ self.update_alert(self._error_alert)
+
+ def update_confirmation_alert(self, title, message, callback,
+ data):
+ self.update_title_and_message(self._confirmation_alert, title,
+ message)
+ self._callback = callback
+ self._data = data
+ self.update_alert(self._confirmation_alert)
+
+ def get_metadata_list(self, selected_state):
+ metadata_list = []
+
+ list_view_model = self.get_list_view().get_model()
+ for index in range(0, len(list_view_model)):
+ metadata = list_view_model.get_metadata(index)
+ if metadata.get('selected', '0') == selected_state:
+ metadata_list.append(metadata)
+
+ return metadata_list
def get_journal():
@@ -378,3 +489,11 @@ def get_journal():
def start():
get_journal()
+
+
+def set_mount_point(mount_point):
+ global _mount_point
+ _mount_point = mount_point
+
+def get_mount_point():
+ return _mount_point
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index 2aa4153..3d7359d 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -1,5 +1,9 @@
# Copyright (C) 2007, One Laptop Per Child
# Copyright (C) 2009, Walter Bender
+# Copyright (C) 2012, Walter Bender <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay at activitycentral.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,6 +20,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+from gettext import ngettext
import logging
from datetime import datetime, timedelta
import os
@@ -43,8 +48,9 @@ from sugar import mime
from jarabe.model import bundleregistry
from jarabe.journal import misc
from jarabe.journal import model
-from jarabe.journal.palettes import ClipboardMenu
-from jarabe.journal.palettes import VolumeMenu
+from jarabe.journal import palettes
+
+COPY_MENU_HELPER = palettes.get_copy_menu_helper()
_AUTOSEARCH_TIMEOUT = 1000
@@ -455,39 +461,11 @@ class EntryToolbar(gtk.Toolbar):
palette.menu.remove(menu_item)
menu_item.destroy()
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=gtk.ICON_SIZE_MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(clipboard_menu)
- clipboard_menu.show()
-
- if self._metadata['mountpoint'] != '/':
- client = gconf.client_get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=gtk.ICON_SIZE_MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(journal_menu)
- journal_menu.show()
-
- volume_monitor = gio.volume_monitor_get()
- icon_theme = gtk.icon_theme_get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=gtk.ICON_SIZE_MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(volume_menu)
- volume_menu.show()
+ COPY_MENU_HELPER.insert_copy_to_menu_items(palette.menu,
+ [self._metadata],
+ show_editing_alert=False,
+ show_progress_info_alert=False,
+ batch_mode=False)
def _refresh_duplicate_palette(self):
color = misc.get_icon_color(self._metadata)
@@ -527,6 +505,201 @@ class EntryToolbar(gtk.Toolbar):
menu_item.show()
+class EditToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.edit_toolbar = EditToolbar()
+ self.add_toolbar('', self.edit_toolbar)
+ self.edit_toolbar.show()
+
+
+class EditToolbar(gtk.Toolbar):
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self.add(SelectNoneButton())
+ self.add(SelectAllButton())
+ self.add(gtk.SeparatorToolItem())
+ self.add(BatchEraseButton())
+ self.add(BatchCopyButton())
+
+ self.show_all()
+
+
+class SelectNoneButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'select-none')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=False,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ switch_to_normal_mode_after_completion=True,
+ show_post_selected_confirmation=True,
+ show_not_completed_ops_info=False)
+ self.props.tooltip = _('Select none')
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _get_editing_alert_operation(self):
+ return _('Select None')
+
+ def _get_info_alert_title(self):
+ return _('Deselecting')
+
+ def _get_post_selection_alert_message(self, entries_len):
+ return ngettext('You have deselected %d entry.',
+ 'You have deselected %d entries.',
+ entries_len) % (entries_len,)
+
+ def _operate(self, metadata):
+ from jarabe.journal.journalactivity import get_journal
+ journal_list_view = get_journal().get_list_view()
+
+ if not self._file_path_valid(metadata):
+ return False
+
+ metadata['selected'] = '0'
+ journal_list_view._process_new_selected_status('0')
+
+ self._metadata_write_valid(metadata)
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class SelectAllButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'select-all')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=False,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=True,
+ switch_to_normal_mode_after_completion=False,
+ show_post_selected_confirmation=True,
+ show_not_completed_ops_info=False)
+ self.props.tooltip = _('Select all')
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _get_editing_alert_operation(self):
+ return _('Select All')
+
+ def _get_info_alert_title(self):
+ return _('Selecting')
+
+ def _get_post_selection_alert_message(self, entries_len):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return ngettext('You have selected %d entry.',
+ 'You have selected %d entries.',
+ entries_len) % (entries_len,)
+
+ def _operate(self, metadata):
+ from jarabe.journal.journalactivity import get_journal
+ journal_list_view = get_journal().get_list_view()
+
+ metadata['selected'] = '1'
+ journal_list_view._process_new_selected_status('1')
+
+ self._metadata_write_valid(metadata)
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchEraseButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-delete')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ switch_to_normal_mode_after_completion=True,
+ show_post_selected_confirmation=False,
+ show_not_completed_ops_info=True)
+ self.props.tooltip = _('Erase')
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _get_editing_alert_title(self):
+ return _('Erase')
+
+ def _get_editing_alert_message(self, entries_len):
+ return ngettext('Do you want to erase %d entry?',
+ 'Do you want to erase %d entries?',
+ entries_len) % (entries_len)
+
+ def _get_editing_alert_operation(self):
+ return _('Erase')
+
+ def _get_info_alert_title(self):
+ return _('Erasing')
+
+ def _operate(self, metadata):
+ model.delete(metadata['uid'])
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchCopyButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-copy')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ need_to_popup_options=True,
+ operate_on_deselected_entries=False,
+ switch_to_normal_mode_after_completion=True,
+ show_post_selected_confirmation=False,
+ show_not_completed_ops_info=False)
+
+ self.props.tooltip = _('Copy')
+
+ self._metadata_list = None
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _fill_and_pop_up_options(self, widget_clicked):
+ for child in self.props.palette.menu.get_children():
+ self.props.palette.menu.remove(child)
+
+ COPY_MENU_HELPER.insert_copy_to_menu_items(self.props.palette.menu,
+ [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True)
+ self.props.palette.popup(immediate=True, state=1)
+
+
+
+
+
+
+
+
+
+class EditCopyItem(MenuItem):
+ __gtype_name__ = 'JournalEditCopyItem'
+
+ def __init__(self, icon_name, text_label, mount_path):
+ MenuItem.__init__(self, icon_name=icon_name, text_label=text_label)
+ self.mount_path = mount_path
+ self.mount_info = text_label
+
class SortingButton(ToolButton):
__gtype_name__ = 'JournalSortingButton'
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
index 417ff61..613f3cf 100644
--- a/src/jarabe/journal/listmodel.py
+++ b/src/jarabe/journal/listmodel.py
@@ -1,4 +1,8 @@
# Copyright (C) 2009, Tomeu Vizoso
+# Copyright (C) 2012, Walter Bender <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay at activitycentral.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -54,6 +58,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
COLUMN_BUDDY_1 = 9
COLUMN_BUDDY_2 = 10
COLUMN_BUDDY_3 = 11
+ COLUMN_SELECT = 12
_COLUMN_TYPES = {
COLUMN_UID: str,
@@ -68,6 +73,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
COLUMN_BUDDY_1: object,
COLUMN_BUDDY_3: object,
COLUMN_BUDDY_2: object,
+ COLUMN_SELECT: bool,
}
_PAGE_SIZE = 10
@@ -198,6 +204,8 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
self._cached_row.append(None)
+ self._cached_row.append(metadata.get('selected', '0') == '1')
+
return self._cached_row[column]
def on_iter_nth_child(self, iterator, n):
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index a0ceccc..1bf9d7f 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -1,4 +1,8 @@
# Copyright (C) 2009, Tomeu Vizoso
+# Copyright (C) 2012, Walter Bender <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay at activitycentral.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -98,6 +102,8 @@ class BaseListView(gtk.Bin):
self._title_column = None
self.sort_column = None
self._add_columns()
+ self._inhibit_refresh = False
+ self._selected_entries = 0
self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
[('text/uri-list', 0, 0),
@@ -134,6 +140,18 @@ class BaseListView(gtk.Bin):
return object_id.startswith(self._query['mountpoints'][0])
def _add_columns(self):
+ cell_select = gtk.CellRendererToggle()
+ cell_select.props.indicator_size = style.zoom(26)
+ cell_select.props.activatable = True
+ cell_select.connect('toggled', self.__selected_cb)
+
+ column = gtk.TreeViewColumn()
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+ column.props.fixed_width = style.GRID_CELL_SIZE
+ column.pack_start(cell_select)
+ column.add_attribute(cell_select, "active", ListModel.COLUMN_SELECT)
+ self.tree_view.append_column(column)
+
cell_favorite = CellRendererFavorite(self.tree_view)
cell_favorite.connect('clicked', self.__favorite_clicked_cb)
@@ -251,6 +269,35 @@ class BaseListView(gtk.Bin):
else:
cell.props.xo_color = None
+ def __selected_cb(self, cell, path):
+ row = self._model[path]
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ if metadata.get('selected', '0') == '1':
+ metadata['selected'] = '0'
+ self._process_new_selected_status('0')
+ else:
+ metadata['selected'] = '1'
+ self._process_new_selected_status('1')
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.set_bundle_installation_allowed(False)
+ model.write(metadata, update_mtime=False)
+ journal.set_bundle_installation_allowed(True)
+
+ def _process_new_selected_status(self, new_status):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if new_status == '0':
+ self._selected_entries = self._selected_entries - 1
+ if self._selected_entries == 0:
+ journal.switch_to_editing_mode(False)
+ else:
+ self._selected_entries = self._selected_entries + 1
+ journal.switch_to_editing_mode(True)
+
def __favorite_clicked_cb(self, cell, path):
row = self._model[path]
metadata = model.get(row[ListModel.COLUMN_UID])
@@ -274,9 +321,14 @@ class BaseListView(gtk.Bin):
ListModel.COLUMN_TIMESTAMP))
self._query = query_dict
+ # This refresh is always needed, since the query has changed.
self.refresh()
def refresh(self):
+ if not self._inhibit_refresh:
+ self.proceed_with_refresh()
+
+ def proceed_with_refresh(self):
logging.debug('ListView.refresh query %r', self._query)
self._stop_progress_bar()
@@ -466,6 +518,12 @@ class BaseListView(gtk.Bin):
self.update_dates()
return True
+ def get_model(self):
+ return self._model
+
+ def inhibit_refresh(self, inhibit):
+ self._inhibit_refresh = inhibit
+
class ListView(BaseListView):
__gtype_name__ = 'JournalListView'
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 5285a7c..39547a4 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -1,4 +1,8 @@
# Copyright (C) 2007-2011, One Laptop per Child
+# Copyright (C) 2012, Walter Bender <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay at activitycentral.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -37,7 +41,6 @@ from sugar import dispatch
from sugar import mime
from sugar import util
-
DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
@@ -45,7 +48,8 @@ DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
# Properties the journal cares about.
PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id',
'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type',
- 'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid']
+ 'mountpoint', 'mtime', 'progress', 'timestamp', 'title',
+ 'uid', 'selected']
MIN_PAGES_TO_CACHE = 3
MAX_PAGES_TO_CACHE = 5
@@ -651,6 +655,11 @@ def write(metadata, file_path='', update_mtime=True, transfer_ownership=True):
file_path,
transfer_ownership)
else:
+ # HACK: For documents: modify the mount-point
+ from jarabe.journal.journalactivity import get_mount_point
+ if get_mount_point() == get_documents_path():
+ metadata['mountpoint'] = get_documents_path()
+
object_id = _write_entry_on_external_device(metadata, file_path)
return object_id
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 27b0b54..f2ce639 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -1,4 +1,8 @@
# Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2012, Walter Bender <walter at sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo at laptop.org>
+# Copyright (C) 2012, Martin Abente <tch at sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay at activitycentral.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,6 +19,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+from gettext import ngettext
import logging
import os
@@ -23,6 +28,9 @@ import gtk
import gconf
import gio
import glib
+import time
+
+from sugar import _sugarext
from sugar.graphics import style
from sugar.graphics.palette import Palette
@@ -39,6 +47,8 @@ from jarabe.journal import model
friends_model = friends.get_model()
+_copy_menu_helper = None
+
class BulkOperationDetails():
@@ -129,7 +139,16 @@ class ObjectPalette(Palette):
menu_item.set_image(icon)
self.menu.append(menu_item)
menu_item.show()
- copy_menu = CopyMenu(metadata)
+ copy_menu = CopyMenu()
+ copy_menu_helper = get_copy_menu_helper()
+
+ metadata_list = []
+ metadata_list.append(metadata)
+ copy_menu_helper.insert_copy_to_menu_items(copy_menu,
+ metadata_list,
+ False,
+ False,
+ False)
copy_menu.connect('volume-error', self.__volume_error_cb)
menu_item.set_submenu(copy_menu)
@@ -260,156 +279,535 @@ class CopyMenu(gtk.Menu):
([str, str])),
}
- def __init__(self, metadata):
+ def __init__(self):
gobject.GObject.__init__(self)
- self._metadata = metadata
-
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=gtk.ICON_SIZE_MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- self.append(clipboard_menu)
- clipboard_menu.show()
-
- from jarabe.journal import journalactivity
- journal_model = journalactivity.get_journal()
- if journal_model.get_mount_point() != model.get_documents_path():
- documents_menu = DocumentsMenu(self._metadata)
- documents_menu.set_image(Icon(icon_name='user-documents',
- icon_size=gtk.ICON_SIZE_MENU))
- documents_menu.connect('volume-error', self.__volume_error_cb)
- self.append(documents_menu)
- documents_menu.show()
-
- if self._metadata['mountpoint'] != '/':
- client = gconf.client_get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=gtk.ICON_SIZE_MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- self.append(journal_menu)
- journal_menu.show()
-
- volume_monitor = gio.volume_monitor_get()
- icon_theme = gtk.icon_theme_get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=gtk.ICON_SIZE_MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- self.append(volume_menu)
- volume_menu.show()
-
- def __volume_error_cb(self, menu_item, message, severity):
- self.emit('volume-error', message, severity)
+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
+ """
-class VolumeMenu(MenuItem):
- __gtype_name__ = 'JournalVolumeMenu'
+ def __init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ need_to_popup_options,
+ operate_on_deselected_entries,
+ switch_to_normal_mode_after_completion,
+ show_post_selected_confirmation,
+ show_not_completed_ops_info):
+ gobject.GObject.__init__(self)
- __gsignals__ = {
- 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([str, str])),
- }
+ self._label = label
+
+ # Make a copy.
+ self._immutable_metadata_list = []
+ for metadata in metadata_list:
+ self._immutable_metadata_list.append(metadata)
+
+ self._metadata_list = metadata_list
+ self._show_progress_info_alert = show_progress_info_alert
+ self._batch_mode = batch_mode
+ self._operate_on_deselected_entries = \
+ operate_on_deselected_entries
+ self._switch_to_normal_mode_after_completion = \
+ switch_to_normal_mode_after_completion
+ self._show_post_selected_confirmation = \
+ show_post_selected_confirmation
+ self._show_not_completed_ops_info = \
+ show_not_completed_ops_info
+
+ actionable_signal = self._get_actionable_signal()
+
+ if need_to_popup_options:
+ self.connect(actionable_signal, self._fill_and_pop_up_options)
+ else:
+ if show_editing_alert:
+ self.connect(actionable_signal, self._show_editing_alert)
+ else:
+ self.connect(actionable_signal, self._pre_operate_per_action)
+
+ def _get_actionable_signal(self):
+ """
+ Some widgets like 'buttons' have 'clicked' as actionable signal;
+ some like 'menuitems' have 'activate' as actionable signal.
+ """
+
+ raise NotImplementedError
+
+ def _fill_and_pop_up_options(self):
+ """
+ Eg. Batch-Copy-Toolbar-button does not do anything by itself
+ useful; but rather pops-up the actual 'copy-to' options.
+ """
+
+ raise NotImplementedError
+
+ def _show_editing_alert(self, widget_clicked):
+ """
+ Upon clicking the actual operation button (eg.
+ Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT
+ Batch-Copy-Toolbar-button, since it does not do anything
+ actually useful, but only pops-up the actual 'copy-to' options.
+ """
+
+ alert_parameters = self._get_editing_alert_parameters()
+ title = alert_parameters[0]
+ message = alert_parameters[1]
+ operation = alert_parameters[2]
+
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_confirmation_alert(title, message,
+ self._pre_operate_per_action,
+ None)
+
+ def _get_editing_alert_parameters(self):
+ """
+ Get the alert parameters for widgets that can show editing
+ alert.
+ """
+
+ self._metadata_list = self._get_metadata_list()
+ entries_len = len(self._metadata_list)
+
+ title = self._get_editing_alert_title()
+ message = self._get_editing_alert_message(entries_len)
+ operation = self._get_editing_alert_operation()
+
+ return (title, message, operation)
+
+ def _get_list_model_len(self):
+ """
+ Get the total length of the model under view.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return len(journal.get_list_view().get_model())
+
+ def _get_metadata_list(self):
+ """
+ For batch-mode, get the metadata list, according to button-type.
+ For eg, Select-All-Toolbar-button operates on non-selected entries;
+ while othere operate on selected-entries.
+
+ For single-mode, simply copy from the
+ "immutable_metadata_list".
+ """
+
+ if self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if self._operate_on_deselected_entries:
+ return journal.get_metadata_list('0')
+ else:
+ return journal.get_metadata_list('1')
+ else:
+ metadata_list = []
+ for metadata in self._immutable_metadata_list:
+ metadata_list.append(metadata)
+ return metadata_list
+
+ 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 _pre_operate_per_action(self, obj, ok_clicked=False):
+ """
+ This is the stage, just before the FIRST metadata gets into its
+ processing cycle.
+ """
+
+ self._inhibit_refresh(True)
+
+ # Show waiting cursor (only for batch mode)
+ if self._batch_mode:
+ self.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+
+ self._skip_all = False
+
+ # Also, get the initial length of the model.
+ self._model_len = self._get_list_model_len()
+
+ # For batch-operations, fetch the metadata list again.
+ self._metadata_list = self._get_metadata_list()
+
+ # Set the initial length of metadata-list.
+ self._metadata_list_initial_len = len(self._metadata_list)
+
+ self._metadata_processed = 0
+
+ # Next, proceed with the metadata
+ self._pre_operate_per_metadata_per_action()
+
+ def _pre_operate_per_metadata_per_action(self):
+ """
+ This is the stage, just before EVERY metadata gets into doing
+ its actual work.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+
+ # If there is still some metadata left, proceed with the
+ # metadata operation.
+ # Else, proceed to post-operations.
+ if len(self._metadata_list) > 0:
+ metadata = self._metadata_list.pop(0)
+
+ # De-select the entry.
+ # For "SelectAll", no need to set to unset an already unset
+ # entry.
+ if not self._operate_on_deselected_entries:
+ metadata['selected'] = '0'
+ get_journal().get_list_view()._process_new_selected_status('0')
+ if not self._metadata_write_valid(metadata):
+ return
+
+ # If info-alert needs to be shown, show the alert, and
+ # arrange for actual operation.
+ # Else, proceed to actual operation directly.
+ if self._show_progress_info_alert:
+ current_len = len(self._metadata_list)
+
+ # TRANS: Do not translate the two %d, and the %s.
+ info_alert_message = _('( %d / %d ) %s') % (
+ self._metadata_list_initial_len - current_len,
+ self._metadata_list_initial_len, metadata['title'])
+
+ get_journal().update_info_alert(self._get_info_alert_title() + ' ...',
+ info_alert_message,
+ self._operate_per_metadata_per_action,
+ metadata)
+ else:
+ self._operate_per_metadata_per_action(metadata)
+ else:
+ self._post_operate_per_action()
+
+ def _get_info_alert_title(self):
+ raise NotImplementedError
+
+ def _operate_per_metadata_per_action(self, metadata):
+ """
+ This is just a code-convenient-function, which allows
+ runtime-overriding. It just delegates to the actual
+ "self._operate" method, the actual which is determined at
+ runtime.
+ """
+
+ # Pass the callback for the post-operation-for-metadata. This
+ # will ensure that async-operations on the metadata are taken
+ # care of.
+ if self._operate(metadata) is False:
+ return
+ else:
+ self._metadata_processed = self._metadata_processed + 1
+
+
+ def _operate(self, metadata):
+ """
+ Actual, core, productive stage for EVERY metadata.
+ """
+
+ raise NotImplementedError
+
+ def _post_operate_per_metadata_per_action(self, metadata):
+ """
+ This is the stage, just after EVERY metadata has been
+ processed.
+ """
+
+ # Call the next ...
+ self._pre_operate_per_metadata_per_action()
+
+ def _post_operate_per_action(self):
+ """
+ This is the stage, just after the LAST metadata has been
+ processed.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ # Initially, set the operating mode to "Editing-Mode". This
+ # will be useful in the case when the volume-toolbuttons need
+ # to be activated only at the very last step.
+ journal.switch_to_editing_mode(True)
+
+ # Show post-operation confirmation message, if applicable.
+ if self._show_post_selected_confirmation:
+ entries_len = self._model_len
+ message = \
+ self._get_post_selection_alert_message(entries_len)
+ journal.update_error_alert(self._get_editing_alert_operation(),
+ message,
+ self._process_switching_mode,
+ None)
+ else:
+ self._process_switching_mode(None, False)
+
+ # Retain the old cursor.
+ if self._batch_mode:
+ self.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
+
+ self._inhibit_refresh(False)
+ journal.get_list_view().refresh()
+
+ def _process_switching_mode(self, metadata, ok_clicked=False):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ # Necessary to do this, when the alert needs to be hidden,
+ # WITHOUT user-intervention.
+ journal.hide_alert()
+
+ # Switch to non-editing mode (if applicable), after
+ # operation is complete.
+ if self._switch_to_normal_mode_after_completion:
+ journal.switch_to_editing_mode(False)
+
+ def _inhibit_refresh(self, inhibit):
+ if self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().get_list_view().inhibit_refresh(inhibit)
+
+ def _refresh(self):
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().get_list_view().refresh()
+
+ def _handle_error_alert(self, error_message, metadata):
+ """
+ This handles any error scenarios. Examples are of entries that
+ display the message "Entries without a file cannot be copied."
+ This is kind of controller-functionl the model-function is
+ "self._set_error_info_alert".
+ """
+
+ if self._skip_all:
+ self._post_operate_per_metadata_per_action(metadata)
+ else:
+ self._set_error_info_alert(error_message, metadata)
+
+ def _set_error_info_alert(self, error_message, metadata):
+ """
+ This method displays the error alert.
+ """
+
+ current_len = len(self._metadata_list)
+
+ # TRANS: Do not translate the two %d, and the three %s.
+ info_alert_message = _('( %d / %d ) Error while %s %s : %s') % (
+ self._metadata_list_initial_len - current_len,
+ self._metadata_list_initial_len,
+ self._get_info_alert_title(),
+ metadata['title'],
+ error_message)
+
+ # Only show the alert, if allowed to.
+ if self._show_not_completed_ops_info:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_error_alert(self._get_info_alert_title() + ' ...',
+ info_alert_message,
+ self._process_error_skipping,
+ metadata)
+ else:
+ self._process_error_skipping(self._skip_all, metadata)
- def __init__(self, metadata, label, mount_point):
- MenuItem.__init__(self, label)
- self._metadata = metadata
- self.connect('activate', self.__copy_to_volume_cb, mount_point)
+ def _process_error_skipping(self, skip_all, metadata):
+ if skip_all:
+ self._skip_all = True
- def __copy_to_volume_cb(self, menu_item, mount_point):
- file_path = model.get_file(self._metadata['uid'])
+ # The operation for the current metadata is finished (kinda
+ # pseudo ...)
+ self._post_operate_per_metadata_per_action(metadata)
+ def _file_path_valid(self, metadata):
+ file_path = model.get_file(metadata['uid'])
if not file_path or not os.path.exists(file_path):
logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+ error_message = _('Entries without a file cannot be copied.')
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self.emit('volume-error', error_message, _('Warning'))
+ return False
+ else:
+ return True
+
+ def _metadata_copy_valid(self, metadata, mount_point):
+ self._set_bundle_installation_allowed(False)
try:
- model.copy(self._metadata, mount_point)
- except IOError, e:
- logging.exception('Error while copying the entry. %s', e.strerror)
- self.emit('volume-error',
- _('Error while copying the entry. %s') % e.strerror,
- _('Error'))
+ model.copy(metadata, mount_point)
+ return True
+ except Exception, e:
+ logging.exception('Error while copying the entry. %s', e)
+ error_message = _('Error while copying the entry. %s') % e
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self.emit('volume-error', error_message, _('Error'))
+ return False
+ finally:
+ self._set_bundle_installation_allowed(True)
+ def _metadata_write_valid(self, metadata):
+ operation = self._get_info_alert_title()
+ self._set_bundle_installation_allowed(False)
-class ClipboardMenu(MenuItem):
- __gtype_name__ = 'JournalClipboardMenu'
+ try:
+ model.write(metadata, update_mtime=False)
+ return True
+ except Exception, e:
+ logging.exception('Error while writing the metadata. %s', e)
+ error_message = _('Error occurred while %s : %s.') % \
+ (operation, e,)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self.emit('volume-error', error_message, _('Error'))
+ return False
+ finally:
+ self._set_bundle_installation_allowed(True)
- __gsignals__ = {
- 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([str, str])),
- }
+ def _set_bundle_installation_allowed(self, allowed):
+ """
+ This method serves only as a "delegating" method.
+ This has been done to aid easy configurability.
+ """
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
- def __init__(self, metadata):
- MenuItem.__init__(self, _('Clipboard'))
+ if self._batch_mode:
+ journal.set_bundle_installation_allowed(allowed)
- self._temp_file_path = None
- self._metadata = metadata
- self.connect('activate', self.__copy_to_clipboard_cb)
- def __copy_to_clipboard_cb(self, menu_item):
- file_path = model.get_file(self._metadata['uid'])
- if not file_path or not os.path.exists(file_path):
- logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+class BaseCopyMenuItem(MenuItem, ActionItem):
+ __gsignals__ = {
+ 'volume-error': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str, str])),
+ }
+
+ def __init__(self, metadata_list, label, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ MenuItem.__init__(self, label)
+ ActionItem.__init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ switch_to_normal_mode_after_completion=True,
+ show_post_selected_confirmation=False,
+ show_not_completed_ops_info=True)
+
+ def _get_actionable_signal(self):
+ return 'activate'
+
+ def _get_editing_alert_title(self):
+ return _('Copy')
+
+ def _get_editing_alert_message(self, entries_len):
+ return ngettext('Do you want to copy %d entry to %s?',
+ 'Do you want to copy %d entries to %s?',
+ entries_len) % (entries_len, self._label)
+
+ def _get_editing_alert_operation(self):
+ return _('Copy')
+
+ def _get_info_alert_title(self):
+ return _('Copying')
+
+
+class VolumeMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, label, mount_point,
+ show_editing_alert, show_progress_info_alert,
+ batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, label,
+ show_editing_alert,
+ show_progress_info_alert, batch_mode)
+ self._mount_point = mount_point
+
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+ if not self._metadata_copy_valid(metadata, self._mount_point):
+ return False
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class ClipboardMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Clipboard'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ self._temp_file_path_list = []
+
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
clipboard = gtk.Clipboard()
clipboard.set_with_data([('text/uri-list', 0, 0)],
self.__clipboard_get_func_cb,
- self.__clipboard_clear_func_cb)
+ self.__clipboard_clear_func_cb,
+ metadata)
- def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info,
+ metadata):
# Get hold of a reference so the temp file doesn't get deleted
- self._temp_file_path = model.get_file(self._metadata['uid'])
+ self._temp_file_path = model.get_file(metadata['uid'])
logging.debug('__clipboard_get_func_cb %r', self._temp_file_path)
selection_data.set_uris(['file://' + self._temp_file_path])
- def __clipboard_clear_func_cb(self, clipboard, data):
+ def __clipboard_clear_func_cb(self, clipboard, metadata):
# Release and delete the temp file
self._temp_file_path = None
+ # This is async-operation; and this is the ending point.
+ self._post_operate_per_metadata_per_action(metadata)
-class DocumentsMenu(MenuItem):
- __gtype_name__ = 'JournalDocumentsMenu'
-
- __gsignals__ = {
- 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([str, str])),
- }
-
- def __init__(self, metadata):
- MenuItem.__init__(self, _('Documents'))
- self._temp_file_path = None
- self._metadata = metadata
- self.connect('activate', self.__copy_to_documents_cb)
+class DocumentsMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
- def __copy_to_documents_cb(self, menu_item):
- file_path = model.get_file(self._metadata['uid'])
- if not file_path or not os.path.exists(file_path):
- logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+ if not self._metadata_copy_valid(metadata,
+ model.get_documents_path()):
+ return False
- model.copy(self._metadata, model.get_documents_path())
+ # This is sync-operation. Call the post-operation now.
+ self._post_operate_per_metadata_per_action(metadata)
class GroupsMenu(gtk.Menu):
@@ -538,3 +936,90 @@ class BuddyPalette(Palette):
icon=buddy_icon)
# TODO: Support actions on buddies, like make friend, invite, etc.
+
+
+
+class CopyMenuHelper(gtk.Menu):
+ __gtype_name__ = 'JournalCopyMenuHelper'
+
+ __gsignals__ = {
+ 'volume-error': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str, str])),
+ }
+
+ def insert_copy_to_menu_items(self, menu, metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode):
+ self._metadata_list = metadata_list
+
+ clipboard_menu = ClipboardMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
+ icon_size=gtk.ICON_SIZE_MENU))
+ clipboard_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(clipboard_menu)
+ clipboard_menu.show()
+
+ from jarabe.journal.journalactivity import get_mount_point
+
+ if get_mount_point() != model.get_documents_path():
+ documents_menu = DocumentsMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ documents_menu.set_image(Icon(icon_name='user-documents',
+ icon_size=gtk.ICON_SIZE_MENU))
+ documents_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(documents_menu)
+ documents_menu.show()
+
+ if get_mount_point() != '/':
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ journal_menu = VolumeMenu(metadata_list, _('Journal'), '/',
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ journal_menu.set_image(Icon(icon_name='activity-journal',
+ xo_color=color,
+ icon_size=gtk.ICON_SIZE_MENU))
+ journal_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(journal_menu)
+ journal_menu.show()
+
+ volume_monitor = gio.volume_monitor_get()
+ icon_theme = gtk.icon_theme_get_default()
+ for mount in volume_monitor.get_mounts():
+ if get_mount_point() == mount.get_root().get_path():
+ continue
+
+ volume_menu = VolumeMenu(metadata_list, mount.get_name(),
+ mount.get_root().get_path(),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ for name in mount.get_icon().props.names:
+ if icon_theme.has_icon(name):
+ volume_menu.set_image(Icon(icon_name=name,
+ icon_size=gtk.ICON_SIZE_MENU))
+ break
+
+ volume_menu.connect('volume-error', self.__volume_error_cb)
+ menu.insert(volume_menu, -1)
+ volume_menu.show()
+
+ def __volume_error_cb(self, menu_item, message, severity):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal._volume_error_cb(menu_item, message, severity)
+
+
+def get_copy_menu_helper():
+ global _copy_menu_helper
+ if _copy_menu_helper is None:
+ _copy_menu_helper = CopyMenuHelper()
+ return _copy_menu_helper
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 1356099..c591cc4 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -297,6 +297,15 @@ class VolumesToolbar(gtk.Toolbar):
button = self._get_button_for_mount(mount)
button.props.active = True
+ def set_volume_buttons_sensitive(self, sensitive, mount_point):
+ """
+ Toggles the state of all volume-buttons, except the currently
+ active mount-point.
+ """
+ for button in self._volume_buttons:
+ if button.mount_point != mount_point:
+ button.set_sensitive(sensitive)
+
class BaseButton(RadioToolButton):
__gsignals__ = {
--
1.7.4.4
More information about the Sugar-devel
mailing list