diff options
author | Ajay Garg <ajay@activitycentral.com> | 2012-02-21 08:37:46 (GMT) |
---|---|---|
committer | Anish Mangal <anish@activitycentral.com> | 2012-04-27 10:02:36 (GMT) |
commit | 1d3574120ac7d7bbc4017aa0d0b2aab19553cd4a (patch) | |
tree | e48f94ed53499aa17d7a659206dc6ddf5916d4a3 | |
parent | 3d4159e4b2e1dd72d914c1c76f058e85f5ee3ec6 (diff) |
uy#1242: Batch Operations on Journal Entries (Copy, Erase)
-rw-r--r-- | src/jarabe/journal/journalactivity.py | 139 | ||||
-rw-r--r-- | src/jarabe/journal/journaltoolbox.py | 238 | ||||
-rw-r--r-- | src/jarabe/journal/listmodel.py | 27 | ||||
-rw-r--r-- | src/jarabe/journal/listview.py | 99 | ||||
-rw-r--r-- | src/jarabe/journal/model.py | 13 | ||||
-rw-r--r-- | src/jarabe/journal/palettes.py | 686 | ||||
-rw-r--r-- | src/jarabe/journal/volumestoolbar.py | 9 |
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__ = { |