From 29726a00f3f0cc1a491f2b0b3c0cec9956854323 Mon Sep 17 00:00:00 2001 From: Ajay Garg Date: Sun, 5 Aug 2012 00:37:33 +0530 Subject: [sugar PATCH] Multi-Select REVIEW changes/fixes Organization: Sugar Labs Foundation Signed-off-by: Ajay Garg --- Following are the changes / fixes. ALL courtesy Gary Martin :) a) 'Select none' renamed as 'Deselect all'. b) Now, a text-widget has been added to the top of EditToolBar. This serves the following two purposes :: * The widget is supposed to display only one line, at ANY time. * Usually, while in "multi-select" mode, it will display " of 97 selected", where "x" is the number of entries currently selected, and 97 is assumed to be the total number of entries. Here, as we select/deselect by single-click, or "select all"/"deselect all" button, the update happens consequently. So, as is obvious, this modification helps show the number of selected entries, even when entries are selected/deselected one at a time (previously, the status was shown, only when "select all" or "deselect all" was done). * During batch-copy, or batch-erase, this widget shows the running status of the entry currently being processed. c) Due to b), the progress-statuses are now NOT shown as alerts; rather as texts in the text-widget. d) However, any errors (such as "Entries without a file cannot be copied") are continued to be shown as alerts. e) Other than the progress-texts, and error-alerts, the only other notification shown are the confirmation-alerts before beginning with the "Batch-Copy" and "Batch-Erase". f) During Batch-Operations (almost exclusively Batch-Copy), if an error occurs, users are presented with two options :: * "Stop" - This stops the batch-operation there and then. * "Continue" - Proceed forward with the next journal entry. g) As seen in f), the "Ok" of the error-alert has been replaced (only textually) by "Continue". h) There were exceptions of the form "KeyError: 'keep'" occuring in logs. This was due to some cases, wherein "keep" property was not present in a particular journal entry. So now, as a fix, we first check if "keep" is a valid metadata-key. If yes, we read its value to gauge favorite-status. Else, we assume that the journal-entry is an unfavorite by default. i) VERY IMPORTANT NOTE :: Renaming a journal-entry (by clicking and modifying the contents of the title-cell, has been disabled functionally. This is because, the following happens when a rename is done in the "Documents" view :: * Initially, the UID is same as the path of the entry in "Documents". * User changes the name. The change is written on the DS, and the UID changed. * Now, since refresh is inhibited in multi-select view, we need to fetch the new value of the title from the DS. This requires the UID, through which the UID could be fetched. Since the name of the "Documents" journal-entry has changed, so has its UID. But in the memory, the old UID still resides. Fetching the "new" title from the "old" UID does not work. Now, I tried disabling the renaming while rendering the listview, but that could not be done, as rendering th listview requires knowing whether we are in multi-select mode, while multi-select mode is set, after the listview is rendered. So, we are in a catch-22 situation. So, the way it works now in multi-select mode :: * User is apparently able to edit the title, but that is all what happens. There is no efective change - neither in backend, nor in frontend. In the normal view, the renaming works as usual. Again, I must emphasize, that each one of the "ticket" has been caught by Gary :), and no one else. Can't thank him enough, as this has helped tremendously im making this feature sleeker and extremely user-friendly. src/jarabe/journal/journalactivity.py | 45 +++++++++++----- src/jarabe/journal/journaltoolbox.py | 89 +++++++++++++++++++++++++++++--- src/jarabe/journal/listview.py | 35 +++++++------ src/jarabe/journal/model.py | 4 +- src/jarabe/journal/palettes.py | 50 +++++++++++------- 5 files changed, 162 insertions(+), 61 deletions(-) diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 1b841e9..4bf76f4 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -31,7 +31,7 @@ import os import gobject from sugar.graphics.window import Window -from sugar.graphics.alert import Alert, ErrorAlert, ConfirmationAlert +from sugar.graphics.alert import Alert, ErrorAlert, ConfirmationAlert, NButtonAlert from sugar.graphics.icon import Icon from sugar.bundle.bundle import ZipExtractException, RegistrationException @@ -134,8 +134,18 @@ class JournalActivity(JournalWindow): self._volumes_toolbar = None self._editing_mode = False self._alert = Alert() - self._error_alert = ErrorAlert() - self._confirmation_alert = ConfirmationAlert() + + self._error_alert = NButtonAlert() + self._error_alert._populate_buttons([ + ('dialog-ok', gtk.RESPONSE_OK, _('Ok')) + ]) + + self._confirmation_alert = NButtonAlert() + self._confirmation_alert._populate_buttons([ + ('dialog-cancel', gtk.RESPONSE_CANCEL, _('Stop')), + ('dialog-ok', gtk.RESPONSE_OK, _('Continue')) + ]) + self._current_alert = None self.setup_handlers_for_alert_actions() @@ -244,12 +254,15 @@ class JournalActivity(JournalWindow): def show_main_view(self): if self._editing_mode: - toolbox = EditToolbox() + self._toolbox = EditToolbox() + + # TRANS: Do not translate the "%d" + self._toolbox.set_total_number_of_entries(self.get_total_number_of_entries()) else: - toolbox = self._main_toolbox + self._toolbox = self._main_toolbox - self.set_toolbar_box(toolbox) - toolbox.show() + self.set_toolbar_box(self._toolbox) + self._toolbox.show() if self.canvas != self._main_view: self.set_canvas(self._main_view) @@ -425,8 +438,8 @@ class JournalActivity(JournalWindow): 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) + gobject.idle_add(self._callback, self._data, + response_id) def update_title_and_message(self, alert, title, message): alert.props.title = title @@ -446,11 +459,8 @@ class JournalActivity(JournalWindow): 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_info_alert(self, title, message): + self.get_toolbar_box().display_running_status_in_multi_select(title, message) def update_error_alert(self, title, message, callback, data): self.update_title_and_message(self._error_alert, title, @@ -482,12 +492,19 @@ class JournalActivity(JournalWindow): return metadata_list + def get_total_number_of_entries(self): + list_view_model = self.get_list_view().get_model() + return len(list_view_model) + def is_editing_mode_present(self): return self._editing_mode def get_volumes_toolbar(self): return self._volumes_toolbar + def get_toolbar_box(self): + return self._toolbox + def get_journal(): global _journal diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 730ad7d..59961be 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -513,6 +513,22 @@ class EditToolbox(Toolbox): self.add_toolbar('', self.edit_toolbar) self.edit_toolbar.show() + def process_new_selected_entry_in_multi_select(self): + self.edit_toolbar._update_info('', '', True, True) + + def process_new_deselected_entry_in_multi_select(self): + self.edit_toolbar._update_info('', '', False, True) + + def display_running_status_in_multi_select(self, primary_info, + secondary_info): + self.edit_toolbar._update_info(primary_info, secondary_info, None, True) + + def display_already_selected_entries_status(self): + self.edit_toolbar._update_info('', '', True, False) + + def set_total_number_of_entries(self, total): + self.edit_toolbar._set_total_number_of_entries(total) + class EditToolbar(gtk.Toolbar): def __init__(self): @@ -520,36 +536,52 @@ class EditToolbar(gtk.Toolbar): self.add(SelectNoneButton()) self.add(SelectAllButton()) + self.add(gtk.SeparatorToolItem()) + self.add(BatchEraseButton()) self.add(BatchCopyButton()) + self.add(gtk.SeparatorToolItem()) + + self._multi_select_info_widget = MultiSelectEntriesInfoWidget() + self.add(self._multi_select_info_widget) + self.show_all() + def _set_total_number_of_entries(self, total): + self._multi_select_info_widget.set_total_number_of_entries(total) + + def _update_info(self, primary_info, secondary_info, + special_action, update_selected_entries): + gobject.idle_add(self._multi_select_info_widget.update_text, + primary_info, secondary_info, + special_action, update_selected_entries) + 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, + show_progress_info_alert=False, batch_mode=True, auto_deselect_source_entries=True, need_to_popup_options=False, operate_on_deselected_entries=False, switch_to_normal_mode_after_completion=True, - show_post_selected_confirmation=True, + show_post_selected_confirmation=False, show_not_completed_ops_info=False) - self.props.tooltip = _('Select none') + self.props.tooltip = _('Deselect all') def _get_actionable_signal(self): return 'clicked' def _get_editing_alert_operation(self): - return _('Select none') + return _('Deselect all') def _get_info_alert_title(self): - return _('Deselecting') + return _('DESELECTING') def _get_post_selection_alert_message_entries_len(self): return self._metadata_list_initial_len @@ -573,13 +605,13 @@ class SelectAllButton(ToolButton, palettes.ActionItem): ToolButton.__init__(self, 'select-all') palettes.ActionItem.__init__(self, '', [], show_editing_alert=False, - show_progress_info_alert=True, + show_progress_info_alert=False, batch_mode=True, auto_deselect_source_entries=True, need_to_popup_options=False, operate_on_deselected_entries=True, switch_to_normal_mode_after_completion=False, - show_post_selected_confirmation=True, + show_post_selected_confirmation=False, show_not_completed_ops_info=False) self.props.tooltip = _('Select all') @@ -590,7 +622,7 @@ class SelectAllButton(ToolButton, palettes.ActionItem): return _('Select all') def _get_info_alert_title(self): - return _('Selecting') + return _('SELECTING') def _get_post_selection_alert_message_entries_len(self): return self._model_len @@ -649,7 +681,7 @@ class BatchEraseButton(ToolButton, palettes.ActionItem): return _('Erase') def _get_info_alert_title(self): - return _('Erasing') + return _('ERASING') def _operate(self, metadata): model.delete(metadata['uid']) @@ -691,7 +723,46 @@ class BatchCopyButton(ToolButton, palettes.ActionItem): self.props.palette.popup(immediate=True, state=1) +class MultiSelectEntriesInfoWidget(gtk.ToolItem): + def __init__(self): + gtk.ToolItem.__init__(self) + self._selected_entries = 0 + + self._label = gtk.Label() + self.add(self._label) + + self.show_all() + + def set_total_number_of_entries(self, total): + self._total = total + + def update_text(self, primary_text, secondary_text, special_action, + update_selected_entries): + # If "special_action" is None, + # we need to display the info, conveyed by + # "primary_message" and "secondary_message" + # + # If "special_action" is True, + # a new entry has been selected. + # + # If "special_action" is False, + # an enrty has been deselected. + if special_action == None: + self._label.set_text(primary_text + secondary_text) + self._label.show() + else: + if update_selected_entries: + if special_action == True: + self._selected_entries = self._selected_entries + 1 + elif special_action == False: + self._selected_entries = self._selected_entries - 1 + + # TRANS: Do not translate the two "%d". + message = _('Selected %d of %d') % (self._selected_entries, + self._total) + self._label.set_text(message) + self._label.show() diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index fda2a69..8f1d446 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -184,8 +184,6 @@ class BaseListView(gtk.Bin): self._title_column.pack_start(self.cell_title) self._title_column.add_attribute(self.cell_title, 'markup', ListModel.COLUMN_TITLE) - self._title_column.set_cell_data_func(self.cell_title, - self.__title_clicked_cb) self.tree_view.append_column(self._title_column) buddies_column = gtk.TreeViewColumn() @@ -319,7 +317,10 @@ class BaseListView(gtk.Bin): return metadata = model.get(uid) - favorite = metadata['keep'] + + favorite = None + if 'keep' in metadata.keys(): + favorite = metadata['keep'] if favorite == '1': client = gconf.client_get_default() @@ -355,28 +356,26 @@ class BaseListView(gtk.Bin): def _process_new_selected_status(self, new_status): from jarabe.journal.journalactivity import get_journal journal = get_journal() + journal_toolbar_box = journal.get_toolbar_box() 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() + journal_toolbar_box.process_new_deselected_entry_in_multi_select() + gobject.idle_add(self._post_backend_processing) else: self._selected_entries = self._selected_entries + 1 journal.get_list_view().inhibit_refresh(True) journal.switch_to_editing_mode(True) + journal.get_toolbar_box().process_new_selected_entry_in_multi_select() - def __title_clicked_cb(self, column, cell, tree_model, tree_iter): - # To effect the UI change, fetch the new value from the DS. - uid = tree_model[tree_iter][ListModel.COLUMN_UID] - if uid is None: - return - - metadata = model.get(uid) - title = metadata['title'] + def _post_backend_processing(self): + from jarabe.journal.journalactivity import get_journal + journal = get_journal() - cell.props.text = title + if self._selected_entries == 0: + journal.switch_to_editing_mode(False) + journal.get_list_view().inhibit_refresh(False) + journal.get_list_view().refresh() def update_with_query(self, query_dict): logging.debug('ListView.update_with_query') @@ -677,6 +676,10 @@ class ListView(BaseListView): misc.resume(metadata) def __cell_title_edited_cb(self, cell, path, new_text): + from jarabe.journal.journalactivity import get_journal + if get_journal().is_editing_mode_present(): + return + row = self._model[path] metadata = model.get(row[ListModel.COLUMN_UID]) metadata['title'] = new_text diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index 1f042db..1c486f8 100644 --- a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@ -709,7 +709,7 @@ def extract_ip_address_from_locally_mounted_remote_share_path(path): def get(object_id): """Returns the metadata for an object """ - if (os.path.exists(object_id) or (is_locally_mounted_remote_share(object_id))): + if (object_id[0] == '/'): """ For Documents/Shares/Mounted-Drives/Locally-Mounted-Remote-Shares, where ".Sugar-Metadata" folder exists. @@ -718,7 +718,7 @@ def get(object_id): "file" is not physically present. """ if os.path.exists(object_id): - # if the file is physically present, derive file-metadata + # if the file is physically present, derive filesize-metadata # by physical examination of the file. stat = os.stat(object_id) else: diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index c96e232..d148a76 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -275,6 +275,7 @@ class ActionItem(gobject.GObject): show_post_selected_confirmation self._show_not_completed_ops_info = \ show_not_completed_ops_info + self._continue_operation = True actionable_signal = self._get_actionable_signal() @@ -433,12 +434,11 @@ class ActionItem(gobject.GObject): 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) + get_journal().update_info_alert(self._get_info_alert_title() + ': ', + info_alert_message) + + # Call the core-function !! + gobject.idle_add(self._operate_per_metadata_per_action, metadata) else: self._post_operate_per_action() @@ -453,14 +453,17 @@ class ActionItem(gobject.GObject): 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 + if self._continue_operation is False: + # Jump directly to the post-operation + self._post_operate_per_metadata_per_action(metadata) else: - self._metadata_processed = self._metadata_processed + 1 - + # 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): """ @@ -469,7 +472,7 @@ class ActionItem(gobject.GObject): raise NotImplementedError - def _post_operate_per_metadata_per_action(self, metadata): + def _post_operate_per_metadata_per_action(self, metadata): """ This is the stage, just after EVERY metadata has been processed. @@ -494,6 +497,7 @@ class ActionItem(gobject.GObject): from jarabe.journal.journalactivity import get_journal journal = get_journal() + journal_toolbar_box = journal.get_toolbar_box() # Show post-operation confirmation message, if applicable. if self._show_post_selected_confirmation: @@ -506,6 +510,9 @@ class ActionItem(gobject.GObject): self._process_switching_mode, None) else: + if not self._auto_deselect_source_entries: + journal_toolbar_box.display_already_selected_entries_status() + self._process_switching_mode(None, False) # Retain the old cursor. @@ -555,16 +562,19 @@ class ActionItem(gobject.GObject): # 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() + ' ...', + get_journal().update_confirmation_alert(self._get_info_alert_title() + ' ...', info_alert_message, self._process_error_skipping, metadata) else: - self._process_error_skipping(self._skip_all, metadata) + self._process_error_skipping(metadata, gtk.RESPONSE_OK) + + def _process_error_skipping(self, metadata, response_id): + # This sets up the decision, as to whether continue operations + # with the rest of the data. + if response_id == gtk.RESPONSE_CANCEL: + self._continue_operation = False - 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): @@ -686,7 +696,7 @@ class BaseCopyMenuItem(MenuItem, ActionItem): return _('Copy') def _get_info_alert_title(self): - return _('Copying') + return _('COPYING') class VolumeMenu(BaseCopyMenuItem): -- 1.7.4.4