Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAjay Garg <ajay@activitycentral.com>2012-02-21 08:37:46 (GMT)
committer Anish Mangal <anish@activitycentral.com>2012-04-27 10:02:36 (GMT)
commit1d3574120ac7d7bbc4017aa0d0b2aab19553cd4a (patch)
treee48f94ed53499aa17d7a659206dc6ddf5916d4a3
parent3d4159e4b2e1dd72d914c1c76f058e85f5ee3ec6 (diff)
uy#1242: Batch Operations on Journal Entries (Copy, Erase)
-rw-r--r--src/jarabe/journal/journalactivity.py139
-rw-r--r--src/jarabe/journal/journaltoolbox.py238
-rw-r--r--src/jarabe/journal/listmodel.py27
-rw-r--r--src/jarabe/journal/listview.py99
-rw-r--r--src/jarabe/journal/model.py13
-rw-r--r--src/jarabe/journal/palettes.py686
-rw-r--r--src/jarabe/journal/volumestoolbar.py9
7 files changed, 1054 insertions, 157 deletions
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index 8cafef0..2e044fc 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@sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
+# Copyright (C) 2012, Martin Abente <tch@sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay@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,87 @@ 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)
+ metadata_selected = \
+ list_view_model.get_selected_value(metadata['uid'])
+
+ if ( (selected_state and metadata_selected) or \
+ ((not selected_state) and (not metadata_selected)) ):
+ metadata_list.append(metadata)
+
+ return metadata_list
def get_journal():
@@ -378,3 +493,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..fd14826 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@sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
+# Copyright (C) 2012, Martin Abente <tch@sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay@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,196 @@ 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_entries_len(self):
+ return self._metadata_list_initial_len
+
+ 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):
+ # Nothing specific needs to be done.
+ # The checkboxes are unchecked as part of the toggling of any
+ # operation that operates on selected entries.
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class 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_entries_len(self):
+ return self._model_len
+
+ def _get_post_selection_alert_message(self, entries_len):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return ngettext('You have selected %d entry.',
+ 'You have selected %d entries.',
+ entries_len) % (entries_len,)
+
+ def _operate(self, metadata):
+ # Nothing specific needs to be done.
+ # The checkboxes are unchecked as part of the toggling of any
+ # operation that operates on selected entries.
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchEraseButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-delete')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ 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..dcc3539 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@sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
+# Copyright (C) 2012, Martin Abente <tch@sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay@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
@@ -78,7 +84,9 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
self._last_requested_index = None
self._cached_row = None
self._result_set = model.find(query, ListModel._PAGE_SIZE)
+ self._selected = {}
self._temp_drag_file_path = None
+ self._uid_metadata_assoc = {}
# HACK: The view will tell us that it is resizing so the model can
# avoid hitting D-Bus and disk.
@@ -93,6 +101,21 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
def __result_set_progress_cb(self, **kwargs):
self.emit('progress')
+ def update_uid_metadata_assoc(self, uid, metadata):
+ self._uid_metadata_assoc[uid] = metadata
+
+ def set_selected_value(self, uid, value):
+ if value == False:
+ del self._selected[uid]
+ elif value == True:
+ self._selected[uid] = value
+
+ def get_selected_value(self, uid):
+ if self._selected.has_key(uid):
+ return True
+ else:
+ return False
+
def setup(self):
self._result_set.setup()
@@ -102,6 +125,10 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
def get_metadata(self, path):
return model.get(self[path][ListModel.COLUMN_UID])
+ def get_in_memory_metadata(self, path):
+ uid = self[path][ListModel.COLUMN_UID]
+ return self._uid_metadata_assoc[uid]
+
def on_get_n_columns(self):
return len(ListModel._COLUMN_TYPES)
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index a0ceccc..5acbae7 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@sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
+# Copyright (C) 2012, Martin Abente <tch@sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay@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
@@ -23,6 +27,7 @@ import gtk
import hippo
import gconf
import pango
+import traceback
from sugar.graphics import style
from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon
@@ -98,6 +103,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 +141,16 @@ class BaseListView(gtk.Bin):
return object_id.startswith(self._query['mountpoints'][0])
def _add_columns(self):
+ cell_select = CellRendererToggle(self.tree_view)
+ cell_select.connect('clicked', self.__cell_select_clicked_cb)
+
+ column = gtk.TreeViewColumn()
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+ column.props.fixed_width = cell_select.props.width
+ column.pack_start(cell_select)
+ column.set_cell_data_func(cell_select, self.__select_set_data_cb)
+ self.tree_view.append_column(column)
+
cell_favorite = CellRendererFavorite(self.tree_view)
cell_favorite.connect('clicked', self.__favorite_clicked_cb)
@@ -242,6 +259,38 @@ class BaseListView(gtk.Bin):
progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
cell.props.visible = progress < 100
+ def __select_set_data_cb(self, column, cell, tree_model, tree_iter):
+ uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+
+ # This UI callback function may be called, when the model is
+ # still not ready.
+ # Since this function just affects the checkbox (only a UI
+ # change), so if the model is still not ready, it is safe to
+ # return at this point.
+ if uid is None:
+ return
+
+ # Hack to associate the cell with the metadata, so that it (the
+ # cell) is available offline as well (example during
+ # batch-operations, when the processing has to be done, without
+ # actually clicking any cell.
+ metadata = model.get(uid)
+ metadata['cell'] = cell
+ tree_model.update_uid_metadata_assoc(uid, metadata)
+
+ self.do_ui_select_change(metadata)
+
+ def do_ui_select_change(self, metadata):
+ tree_model = self.get_model()
+ selected = tree_model.get_selected_value(metadata['uid'])
+
+ if 'cell' in metadata.keys():
+ cell = metadata['cell']
+ if selected:
+ cell.props.icon_name = 'emblem-checked'
+ else:
+ cell.props.icon_name = 'emblem-unchecked'
+
def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter):
favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
if favorite:
@@ -262,6 +311,34 @@ class BaseListView(gtk.Bin):
metadata['keep'] = '1'
model.write(metadata, update_mtime=False)
+ def __cell_select_clicked_cb(self, cell, path):
+ row = self._model[path]
+ treeiter = self._model.get_iter(path)
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ self.do_backend_select_change(metadata)
+
+ def do_backend_select_change(self, metadata):
+ uid = metadata['uid']
+ selected = self._model.get_selected_value(uid)
+
+ self._model.set_selected_value(uid, not selected)
+ self._process_new_selected_status(not selected)
+
+ def _process_new_selected_status(self, new_status):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if new_status == False:
+ self._selected_entries = self._selected_entries - 1
+ if self._selected_entries == 0:
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+ journal.get_list_view().refresh()
+ else:
+ self._selected_entries = self._selected_entries + 1
+ journal.get_list_view().inhibit_refresh(True)
+ journal.switch_to_editing_mode(True)
+
def update_with_query(self, query_dict):
logging.debug('ListView.update_with_query')
if 'order_by' not in query_dict:
@@ -274,9 +351,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 +548,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'
@@ -550,6 +638,17 @@ class ListView(BaseListView):
def __editing_canceled_cb(self, cell):
self.cell_title.props.editable = False
+class CellRendererToggle(CellRendererIcon):
+ __gtype_name__ = 'JournalCellRendererSelect'
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.SMALL_ICON_SIZE
+ self.props.icon_name = 'checkbox-unchecked'
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
class CellRendererFavorite(CellRendererIcon):
__gtype_name__ = 'JournalCellRendererFavorite'
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 5285a7c..83e216f 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@sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
+# Copyright (C) 2012, Martin Abente <tch@sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay@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']
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..56275e7 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@sugarlabs.org>
+# Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org>
+# Copyright (C) 2012, Martin Abente <tch@sugarlabs.org>
+# Copyright (C) 2012, Ajay Garg <ajay@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,512 @@ class CopyMenu(gtk.Menu):
([str, str])),
}
- def __init__(self, metadata):
+ def __init__(self):
gobject.GObject.__init__(self)
- self._metadata = metadata
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=gtk.ICON_SIZE_MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- self.append(clipboard_menu)
- clipboard_menu.show()
+class ActionItem(gobject.GObject):
+ """
+ This class implements the course of actions that happens when clicking
+ upon an Action-Item (eg. Batch-Copy-Toolbar-button;
+ Actual-Batch-Copy-To-Journal-button;
+ Actual-Batch-Copy-To-Documents-button;
+ Actual-Batch-Copy-To-Mounted-Drive-button;
+ Actual-Batch-Copy-To-Clipboard-button;
+ Single-Copy-To-Journal-button;
+ Single-Copy-To-Documents-button;
+ Single-Copy-To-Mounted-Drive-button;
+ Single-Copy-To-Clipboard-button;
+ Batch-Erase-Button;
+ Select-None-Toolbar-button;
+ Select-All-Toolbar-button
+ """
- from jarabe.journal import journalactivity
- journal_model = journalactivity.get_journal()
- if journal_model.get_mount_point() != model.get_documents_path():
- documents_menu = DocumentsMenu(self._metadata)
- documents_menu.set_image(Icon(icon_name='user-documents',
- icon_size=gtk.ICON_SIZE_MENU))
- documents_menu.connect('volume-error', self.__volume_error_cb)
- self.append(documents_menu)
- documents_menu.show()
+ def __init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ need_to_popup_options,
+ operate_on_deselected_entries,
+ switch_to_normal_mode_after_completion,
+ show_post_selected_confirmation,
+ show_not_completed_ops_info):
+ gobject.GObject.__init__(self)
- if self._metadata['mountpoint'] != '/':
- client = gconf.client_get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=gtk.ICON_SIZE_MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- self.append(journal_menu)
- journal_menu.show()
+ self._label = label
+
+ # Make a copy.
+ self._immutable_metadata_list = []
+ for metadata in metadata_list:
+ self._immutable_metadata_list.append(metadata)
+
+ self._metadata_list = metadata_list
+ self._show_progress_info_alert = show_progress_info_alert
+ self._batch_mode = batch_mode
+ self._operate_on_deselected_entries = \
+ operate_on_deselected_entries
+ self._switch_to_normal_mode_after_completion = \
+ switch_to_normal_mode_after_completion
+ self._show_post_selected_confirmation = \
+ show_post_selected_confirmation
+ self._show_not_completed_ops_info = \
+ show_not_completed_ops_info
+
+ actionable_signal = self._get_actionable_signal()
+
+ if need_to_popup_options:
+ self.connect(actionable_signal, self._fill_and_pop_up_options)
+ else:
+ if show_editing_alert:
+ self.connect(actionable_signal, self._show_editing_alert)
+ else:
+ self.connect(actionable_signal, self._pre_operate_per_action)
+
+ def _get_actionable_signal(self):
+ """
+ Some widgets like 'buttons' have 'clicked' as actionable signal;
+ some like 'menuitems' have 'activate' as actionable signal.
+ """
+
+ raise NotImplementedError
+
+ def _fill_and_pop_up_options(self):
+ """
+ Eg. Batch-Copy-Toolbar-button does not do anything by itself
+ useful; but rather pops-up the actual 'copy-to' options.
+ """
+
+ raise NotImplementedError
+
+ def _show_editing_alert(self, widget_clicked):
+ """
+ Upon clicking the actual operation button (eg.
+ Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT
+ Batch-Copy-Toolbar-button, since it does not do anything
+ actually useful, but only pops-up the actual 'copy-to' options.
+ """
+
+ alert_parameters = self._get_editing_alert_parameters()
+ title = alert_parameters[0]
+ message = alert_parameters[1]
+ operation = alert_parameters[2]
+
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_confirmation_alert(title, message,
+ self._pre_operate_per_action,
+ None)
+
+ def _get_editing_alert_parameters(self):
+ """
+ Get the alert parameters for widgets that can show editing
+ alert.
+ """
+
+ self._metadata_list = self._get_metadata_list()
+ entries_len = len(self._metadata_list)
+
+ title = self._get_editing_alert_title()
+ message = self._get_editing_alert_message(entries_len)
+ operation = self._get_editing_alert_operation()
+
+ return (title, message, operation)
+
+ def _get_list_model_len(self):
+ """
+ Get the total length of the model under view.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return len(journal.get_list_view().get_model())
+
+ def _get_metadata_list(self):
+ """
+ For batch-mode, get the metadata list, according to button-type.
+ For eg, Select-All-Toolbar-button operates on non-selected entries;
+ while othere operate on selected-entries.
+
+ For single-mode, simply copy from the
+ "immutable_metadata_list".
+ """
+
+ if self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if self._operate_on_deselected_entries:
+ return journal.get_metadata_list(False)
+ else:
+ return journal.get_metadata_list(True)
+ else:
+ metadata_list = []
+ for metadata in self._immutable_metadata_list:
+ metadata_list.append(metadata)
+ return metadata_list
- volume_monitor = gio.volume_monitor_get()
- icon_theme = gtk.icon_theme_get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=gtk.ICON_SIZE_MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- self.append(volume_menu)
- volume_menu.show()
+ def _get_editing_alert_title(self):
+ raise NotImplementedError
- def __volume_error_cb(self, menu_item, message, severity):
- self.emit('volume-error', message, severity)
+ def _get_editing_alert_message(self, entries_len):
+ raise NotImplementedError
+ def _get_editing_alert_operation(self):
+ raise NotImplementedError
-class VolumeMenu(MenuItem):
- __gtype_name__ = 'JournalVolumeMenu'
+ def _is_metadata_list_empty(self):
+ return (self._metadata_list is None) or \
+ (len(self._metadata_list) == 0)
- __gsignals__ = {
- 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([str, str])),
- }
+ def _pre_operate_per_action(self, obj, ok_clicked=False):
+ """
+ This is the stage, just before the FIRST metadata gets into its
+ processing cycle.
+ """
- def __init__(self, metadata, label, mount_point):
- MenuItem.__init__(self, label)
- self._metadata = metadata
- self.connect('activate', self.__copy_to_volume_cb, mount_point)
+ # Show waiting cursor (only for batch mode)
+ if self._batch_mode:
+ self.get_root_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
- def __copy_to_volume_cb(self, menu_item, mount_point):
- file_path = model.get_file(self._metadata['uid'])
+ 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)
+
+ # 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.
+ """
+
+ # Toggle the corresponding checkbox - but only for batch-mode.
+ if self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ list_view = get_journal().get_list_view()
+
+ list_view.do_ui_select_change(metadata)
+ list_view.do_backend_select_change(metadata)
+
+ # Call the next ...
+ self._pre_operate_per_metadata_per_action()
+
+ def _post_operate_per_action(self):
+ """
+ This is the stage, just after the LAST metadata has been
+ processed.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ # Show post-operation confirmation message, if applicable.
+ if self._show_post_selected_confirmation:
+ entries_len = \
+ self._get_post_selection_alert_message_entries_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))
+
+ def _process_switching_mode(self, metadata, ok_clicked=False):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ # Necessary to do this, when the alert needs to be hidden,
+ # WITHOUT user-intervention.
+ journal.hide_alert()
+
+ def _refresh(self):
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().get_list_view().refresh()
+
+ def _handle_error_alert(self, error_message, metadata):
+ """
+ This handles any error scenarios. Examples are of entries that
+ display the message "Entries without a file cannot be copied."
+ This is kind of controller-functionl the model-function is
+ "self._set_error_info_alert".
+ """
+
+ if self._skip_all:
+ self._post_operate_per_metadata_per_action(metadata)
+ else:
+ self._set_error_info_alert(error_message, metadata)
+
+ def _set_error_info_alert(self, error_message, metadata):
+ """
+ This method displays the error alert.
+ """
+
+ current_len = len(self._metadata_list)
+
+ # TRANS: Do not translate the two %d, and the three %s.
+ info_alert_message = _('( %d / %d ) Error while %s %s : %s') % (
+ self._metadata_list_initial_len - current_len,
+ self._metadata_list_initial_len,
+ self._get_info_alert_title(),
+ metadata['title'],
+ error_message)
+
+ # Only show the alert, if allowed to.
+ if self._show_not_completed_ops_info:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_error_alert(self._get_info_alert_title() + ' ...',
+ info_alert_message,
+ self._process_error_skipping,
+ metadata)
+ else:
+ self._process_error_skipping(self._skip_all, metadata)
+
+ def _process_error_skipping(self, metadata, skip_all):
+ # 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])),
- }
+class DocumentsMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
- def __init__(self, metadata):
- MenuItem.__init__(self, _('Documents'))
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+ if not self._metadata_copy_valid(metadata,
+ model.get_documents_path()):
+ return False
- self._temp_file_path = None
- self._metadata = metadata
- self.connect('activate', self.__copy_to_documents_cb)
-
- def __copy_to_documents_cb(self, menu_item):
- file_path = model.get_file(self._metadata['uid'])
- if not file_path or not os.path.exists(file_path):
- logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
-
- model.copy(self._metadata, model.get_documents_path())
+ # This is sync-operation. Call the post-operation now.
+ self._post_operate_per_metadata_per_action(metadata)
class GroupsMenu(gtk.Menu):
@@ -538,3 +913,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__ = {