Ticket #3317: 0001-sugar-sl-3317.patch
File 0001-sugar-sl-3317.patch, 50.4 KB (added by ajay_garg, 12 years ago) |
---|
-
src/jarabe/journal/journalactivity.py
From 8733a8cf9fc4a05564a5fc6f328b8f2a077419b1 Mon Sep 17 00:00:00 2001 From: Ajay Garg <ajay@activitycentral.com> Date: Mon, 6 Feb 2012 01:29:12 +0530 Subject: [sugar PATCH] sl#3317: Batch Operations on Journal Entries (Copy, Erase) Organization: Sugar Labs Foundation Signed-off-by: Ajay Garg <ajay@activitycentral.com> --- Note that this patch MUST be applied after the applying of patch at : http://patchwork.sugarlabs.org/patch/1157/ == This code has been written almost exclusively by Martin Abente. == Design discussions at : http://wiki.sugarlabs.org/go/Features/Multi_selection == Screenshots at : http://wiki.sugarlabs.org/go/Features/Multi_selection_screenshots == Martin's work's video at : http://www.sugarlabs.org/~tch/journal2.mpeg Following are the changes/enhancements from Martin's work : a. More copy-to options :: Clipboard, Documents (in addition to mounted drives). b. After entries are copied to another location, both - the source and the target - entries are de-selected automatically, without the user explicitly have to de-select them all manually. c. There has been a progress bar added for batch-operations. Codewise, effore has been put to have maximum code-reuse; and mimimal code-duplication, as most of the operation-parts are same, irrespective of the operation performed. src/jarabe/journal/journalactivity.py | 135 ++++++++- src/jarabe/journal/journaltoolbox.py | 176 ++++++++++- src/jarabe/journal/listmodel.py | 13 + src/jarabe/journal/listview.py | 48 +++ src/jarabe/journal/model.py | 13 +- src/jarabe/journal/palettes.py | 574 +++++++++++++++++++++++++++------ 6 files changed, 844 insertions(+), 115 deletions(-) diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 8cafef0..6f03a73 100644
a b 1 1 # Copyright (C) 2006, Red Hat, Inc. 2 2 # Copyright (C) 2007, One Laptop Per Child 3 # Copyright (C) 2012, Walter Bender <walter@sugarlabs.org> 4 # Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org> 5 # Copyright (C) 2012, Martin Abente <tch@sugarlabs.org> 6 # Copyright (C) 2012, Ajay Garg <ajay@activitycentral.com> 3 7 # 4 8 # This program is free software; you can redistribute it and/or modify 5 9 # it under the terms of the GNU General Public License as published by … … 17 21 18 22 import logging 19 23 from gettext import gettext as _ 24 from gettext import ngettext 20 25 import uuid 21 26 22 27 import gtk 23 28 import dbus 24 29 import statvfs 25 30 import os 31 import gobject 26 32 27 33 from sugar.graphics.window import Window 28 from sugar.graphics.alert import ErrorAlert 34 from sugar.graphics.alert import Alert, ErrorAlert 35 from sugar.graphics.icon import Icon 29 36 30 37 from sugar.bundle.bundle import ZipExtractException, RegistrationException 31 38 from sugar import env … … from sugar import wm 34 41 35 42 from jarabe.model import bundleregistry 36 43 from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox 44 from jarabe.journal.journaltoolbox import EditToolbox 37 45 from jarabe.journal.listview import ListView 46 from jarabe.journal.listmodel import ListModel 38 47 from jarabe.journal.detailview import DetailView 39 48 from jarabe.journal.volumestoolbar import VolumesToolbar 40 49 from jarabe.journal import misc … … _SPACE_TRESHOLD = 52428800 53 62 _BUNDLE_ID = 'org.laptop.JournalActivity' 54 63 55 64 _journal = None 65 _mount_point = None 56 66 57 67 58 68 class JournalActivityDBusService(dbus.service.Object): … … class JournalActivity(JournalWindow): 119 129 self._list_view = None 120 130 self._detail_view = None 121 131 self._main_toolbox = None 132 self._edit_toolbox = None 122 133 self._detail_toolbox = None 123 134 self._volumes_toolbar = None 135 self._editing_mode = False 136 self._editing_alert = None 137 self._info_alert = None 138 self._selected_entries = [] 139 140 set_mount_point('/') 124 141 125 142 self._setup_main_view() 126 143 self._setup_secondary_view() … … class JournalActivity(JournalWindow): 184 201 search_toolbar = self._main_toolbox.search_toolbar 185 202 search_toolbar.connect('query-changed', self._query_changed_cb) 186 203 search_toolbar.set_mount_point('/') 187 se lf._mount_point = '/'204 set_mount_point('/') 188 205 189 206 def _setup_secondary_view(self): 190 207 self._secondary_view = gtk.VBox() … … class JournalActivity(JournalWindow): 217 234 self.show_main_view() 218 235 219 236 def show_main_view(self): 220 if self.toolbar_box != self._main_toolbox: 221 self.set_toolbar_box(self._main_toolbox) 222 self._main_toolbox.show() 237 if self._editing_mode: 238 toolbox = EditToolbox() 239 else: 240 toolbox = self._main_toolbox 241 242 self.set_toolbar_box(toolbox) 243 toolbox.show() 223 244 224 245 if self.canvas != self._main_view: 225 246 self.set_canvas(self._main_view) … … class JournalActivity(JournalWindow): 254 275 def __volume_changed_cb(self, volume_toolbar, mount_point): 255 276 logging.debug('Selected volume: %r.', mount_point) 256 277 self._main_toolbox.search_toolbar.set_mount_point(mount_point) 257 se lf._mount_point = mount_point278 set_mount_point(mount_point) 258 279 self._main_toolbox.set_current_toolbar(0) 259 280 260 281 def __model_created_cb(self, sender, **kwargs): … … class JournalActivity(JournalWindow): 364 385 self.show_main_view() 365 386 self.search_grab_focus() 366 387 367 def get_mount_point(self): 368 return self._mount_point 388 def switch_to_editing_mode(self, switch): 389 # (re)-switch, only if not already. 390 if (switch) and (not self._editing_mode): 391 self._editing_mode = True 392 self.show_main_view() 393 elif (not switch) and (self._editing_mode): 394 self._editing_mode = False 395 self.show_main_view() 396 397 def get_list_view(self): 398 return self._list_view 399 400 def remove_editing_alert(self): 401 if self._editing_alert is not None: 402 self.remove_alert(self._editing_alert) 403 self._editing_alert = None 404 405 def add_editing_alert(self, widget_clicked, title, message, operation, 406 callback): 407 cancel_icon = Icon(icon_name='dialog-cancel') 408 ok_icon = Icon(icon_name='dialog-ok') 409 410 alert = Alert() 411 alert.props.title = title 412 alert.props.msg = message 413 alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon) 414 alert.add_button(gtk.RESPONSE_OK, operation, ok_icon) 415 alert.connect('response', self.__check_for_action, callback) 416 alert.show() 417 418 self.remove_editing_alert() 419 420 self._editing_alert = alert 421 self.add_alert(alert) 422 423 def __check_for_action(self, alert, response_id, callback): 424 self.remove_editing_alert() 425 if response_id == gtk.RESPONSE_OK: 426 gobject.idle_add(callback, None) 427 428 def remove_info_alert(self): 429 if self._info_alert is not None: 430 self.remove_alert(self._info_alert) 431 logging.debug('alert removed') 432 self._info_alert = None 433 434 def add_info_alert(self, button, title, message, show_skip_options, 435 callback, data): 436 skip_icon = Icon(icon_name='dialog-cancel') 437 skip_all_icon = Icon(icon_name='dialog-cancel') 438 439 alert = Alert() 440 alert.props.title = title 441 alert.props.msg = message 442 443 if show_skip_options: 444 alert.add_button(gtk.RESPONSE_CANCEL, _('OK'), skip_icon) 445 446 # Let the user explicitly see each message of the 447 # non-operatable entry. 448 #alert.add_button(gtk.RESPONSE_OK, _('Do not notify again'), skip_all_icon) 449 450 alert.connect('response', self.__check_for_skip_action, 451 callback, data) 452 453 alert.show() 454 self.remove_info_alert() 455 self._info_alert = alert 456 457 if show_skip_options: 458 self.add_alert(alert) 459 else: 460 self.add_alert_and_callback(alert, callback, data) 461 462 def __check_for_skip_action(self, alert, response_id, callback, 463 metadata): 464 self.remove_editing_alert() 465 if response_id == gtk.RESPONSE_OK: 466 gobject.idle_add(callback, True, metadata) 467 elif response_id == gtk.RESPONSE_CANCEL: 468 gobject.idle_add(callback, False, metadata) 469 470 def get_metadata_list(self, selected_state): 471 metadata_list = [] 472 473 list_view_model = self.get_list_view().get_model() 474 for index in range(0, len(list_view_model)): 475 metadata = list_view_model.get_metadata(index) 476 if metadata.get('selected', '0') == selected_state: 477 metadata_list.append(metadata) 478 479 return metadata_list 369 480 370 481 371 482 def get_journal(): … … def get_journal(): 378 489 379 490 def start(): 380 491 get_journal() 492 493 494 def set_mount_point(mount_point): 495 global _mount_point 496 _mount_point = mount_point 497 498 def get_mount_point(): 499 return _mount_point -
src/jarabe/journal/journaltoolbox.py
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 2aa4153..7e1419a 100644
a b 1 1 # Copyright (C) 2007, One Laptop Per Child 2 2 # Copyright (C) 2009, Walter Bender 3 # Copyright (C) 2012, Walter Bender <walter@sugarlabs.org> 4 # Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org> 5 # Copyright (C) 2012, Martin Abente <tch@sugarlabs.org> 6 # Copyright (C) 2012, Ajay Garg <ajay@activitycentral.com> 3 7 # 4 8 # This program is free software; you can redistribute it and/or modify 5 9 # it under the terms of the GNU General Public License as published by … … 16 20 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 21 18 22 from gettext import gettext as _ 23 from gettext import ngettext 19 24 import logging 20 25 from datetime import datetime, timedelta 21 26 import os … … from sugar import mime 43 48 from jarabe.model import bundleregistry 44 49 from jarabe.journal import misc 45 50 from jarabe.journal import model 46 from jarabe.journal.palettes import ClipboardMenu 47 from jarabe.journal.palettes import VolumeMenu 51 from jarabe.journal import palettes 48 52 49 53 50 54 _AUTOSEARCH_TIMEOUT = 1000 … … _ACTION_MY_FRIENDS = 1 63 67 _ACTION_MY_CLASS = 2 64 68 65 69 70 COPY_MENU_HELPER = palettes.get_copy_menu_helper() 71 66 72 class MainToolbox(Toolbox): 67 73 def __init__(self): 68 74 Toolbox.__init__(self) … … class EntryToolbar(gtk.Toolbar): 527 533 menu_item.show() 528 534 529 535 536 class EditToolbox(Toolbox): 537 def __init__(self): 538 Toolbox.__init__(self) 539 540 self.edit_toolbar = EditToolbar() 541 self.add_toolbar('', self.edit_toolbar) 542 self.edit_toolbar.show() 543 544 545 class EditToolbar(gtk.Toolbar): 546 def __init__(self): 547 gtk.Toolbar.__init__(self) 548 549 self.add(SelectNoneButton()) 550 self.add(SelectAllButton()) 551 self.add(gtk.SeparatorToolItem()) 552 self.add(BatchEraseButton()) 553 self.add(BatchCopyButton()) 554 555 self.show_all() 556 557 558 class SelectNoneButton(ToolButton, palettes.ActionItem): 559 def __init__(self): 560 ToolButton.__init__(self, 'select-none') 561 palettes.ActionItem.__init__(self, '', None, 562 show_editing_alert=False, 563 show_progress_info_alert=True, 564 batch_mode=True, 565 need_to_popup_options=False, 566 operate_on_deselected_entries=False, 567 switch_to_normal_mode_after_completion=True) 568 self.props.tooltip = _('Select none') 569 570 def _get_actionable_signal(self): 571 return 'clicked' 572 573 def _get_info_alert_title(self): 574 return _('Deselecting') 575 576 def _operate(self, metadata): 577 metadata['selected'] = '0' 578 model.write(metadata, update_mtime=False) 579 580 # This is sync-operation. Thus, call the callback. 581 self._post_operate_per_metadata_per_action(metadata) 582 583 584 class SelectAllButton(ToolButton, palettes.ActionItem): 585 def __init__(self): 586 ToolButton.__init__(self, 'select-all') 587 palettes.ActionItem.__init__(self, '', None, 588 show_editing_alert=False, 589 show_progress_info_alert=True, 590 batch_mode=True, 591 need_to_popup_options=False, 592 operate_on_deselected_entries=True, 593 switch_to_normal_mode_after_completion=False) 594 self.props.tooltip = _('Select all') 595 596 def _get_actionable_signal(self): 597 return 'clicked' 598 599 def _get_info_alert_title(self): 600 return _('Selecting') 601 602 def _operate(self, metadata): 603 metadata['selected'] = '1' 604 605 file_path = model.get_file(metadata['uid']) 606 if not file_path or not os.path.exists(file_path): 607 logging.warn('Entries without a file cannot be copied.') 608 error_message = _('Entries without a file cannot be copied.') 609 if self._batch_mode: 610 self._handle_error_alert(error_message, metadata) 611 else: 612 self.emit('volume-error', error_message, _('Warning')) 613 return 614 615 model.write(metadata, update_mtime=False) 616 617 # This is sync-operation. Thus, call the callback. 618 self._post_operate_per_metadata_per_action(metadata) 619 620 621 class BatchEraseButton(ToolButton, palettes.ActionItem): 622 def __init__(self): 623 ToolButton.__init__(self, 'edit-delete') 624 palettes.ActionItem.__init__(self, '', None, 625 show_editing_alert=True, 626 show_progress_info_alert=True, 627 batch_mode=True, 628 need_to_popup_options=False, 629 operate_on_deselected_entries=False, 630 switch_to_normal_mode_after_completion=True) 631 self.props.tooltip = _('Erase') 632 633 def _get_actionable_signal(self): 634 return 'clicked' 635 636 def _get_editing_alert_title(self): 637 return _('Erase') 638 639 def _get_editing_alert_message(self, entries_len): 640 return ngettext('Do you want to erase %d entry?', 641 'Do you want to erase %d entries?', 642 entries_len) % (entries_len) 643 644 def _get_editing_alert_operation(self): 645 return _('Erase') 646 647 def _get_info_alert_title(self): 648 return _('Erasing') 649 650 def _operate(self, metadata): 651 model.delete(metadata['uid']) 652 653 # This is sync-operation. Thus, call the callback. 654 self._post_operate_per_metadata_per_action(metadata) 655 656 657 class BatchCopyButton(ToolButton, palettes.ActionItem): 658 def __init__(self): 659 ToolButton.__init__(self, 'edit-copy') 660 palettes.ActionItem.__init__(self, '', None, 661 show_editing_alert=True, 662 show_progress_info_alert=True, 663 batch_mode=True, 664 need_to_popup_options=True, 665 operate_on_deselected_entries=False, 666 switch_to_normal_mode_after_completion=True) 667 668 self.props.tooltip = _('Copy') 669 670 self._metadata_list = None 671 672 def _get_actionable_signal(self): 673 return 'clicked' 674 675 def _fill_and_pop_up_options(self, widget_clicked): 676 for child in self.props.palette.menu.get_children(): 677 self.props.palette.menu.remove(child) 678 679 COPY_MENU_HELPER.insert_copy_to_menu_items(self.props.palette.menu, 680 None, 681 show_editing_alert=True, 682 show_progress_info_alert=True, 683 batch_mode=True) 684 self.props.palette.popup(immediate=True, state=1) 685 686 687 688 689 690 691 692 693 694 class EditCopyItem(MenuItem): 695 __gtype_name__ = 'JournalEditCopyItem' 696 697 def __init__(self, icon_name, text_label, mount_path): 698 MenuItem.__init__(self, icon_name=icon_name, text_label=text_label) 699 self.mount_path = mount_path 700 self.mount_info = text_label 701 530 702 class SortingButton(ToolButton): 531 703 __gtype_name__ = 'JournalSortingButton' 532 704 -
src/jarabe/journal/listmodel.py
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py index 417ff61..a07f897 100644
a b 1 1 # Copyright (C) 2009, Tomeu Vizoso 2 # Copyright (C) 2012, Walter Bender <walter@sugarlabs.org> 3 # Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org> 4 # Copyright (C) 2012, Martin Abente <tch@sugarlabs.org> 5 # Copyright (C) 2012, Ajay Garg <ajay@activitycentral.com> 2 6 # 3 7 # This program is free software; you can redistribute it and/or modify 4 8 # it under the terms of the GNU General Public License as published by … … class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 54 58 COLUMN_BUDDY_1 = 9 55 59 COLUMN_BUDDY_2 = 10 56 60 COLUMN_BUDDY_3 = 11 61 COLUMN_SELECT = 12 57 62 58 63 _COLUMN_TYPES = { 59 64 COLUMN_UID: str, … … class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 68 73 COLUMN_BUDDY_1: object, 69 74 COLUMN_BUDDY_3: object, 70 75 COLUMN_BUDDY_2: object, 76 COLUMN_SELECT: bool, 71 77 } 72 78 73 79 _PAGE_SIZE = 10 … … class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 198 204 199 205 self._cached_row.append(None) 200 206 207 # If an entry was already selected, switch to editing mode. 208 if metadata.get('selected', '0') == '1': 209 from jarabe.journal.journalactivity import get_journal 210 get_journal().switch_to_editing_mode(True) 211 212 self._cached_row.append(metadata.get('selected', '0') == '1') 213 201 214 return self._cached_row[column] 202 215 203 216 def on_iter_nth_child(self, iterator, n): -
src/jarabe/journal/listview.py
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index a0ceccc..2aa85ae 100644
a b 1 1 # Copyright (C) 2009, Tomeu Vizoso 2 # Copyright (C) 2012, Walter Bender <walter@sugarlabs.org> 3 # Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org> 4 # Copyright (C) 2012, Martin Abente <tch@sugarlabs.org> 5 # Copyright (C) 2012, Ajay Garg <ajay@activitycentral.com> 2 6 # 3 7 # This program is free software; you can redistribute it and/or modify 4 8 # it under the terms of the GNU General Public License as published by … … class BaseListView(gtk.Bin): 98 102 self._title_column = None 99 103 self.sort_column = None 100 104 self._add_columns() 105 self._inhibit_refresh = False 106 self._selected_entries = 0 101 107 102 108 self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, 103 109 [('text/uri-list', 0, 0), … … class BaseListView(gtk.Bin): 134 140 return object_id.startswith(self._query['mountpoints'][0]) 135 141 136 142 def _add_columns(self): 143 cell_select = gtk.CellRendererToggle() 144 cell_select.props.indicator_size = style.zoom(26) 145 cell_select.props.activatable = True 146 cell_select.connect('toggled', self.__selected_cb) 147 148 column = gtk.TreeViewColumn() 149 column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED 150 column.props.fixed_width = style.GRID_CELL_SIZE 151 column.pack_start(cell_select) 152 column.add_attribute(cell_select, "active", ListModel.COLUMN_SELECT) 153 self.tree_view.append_column(column) 154 137 155 cell_favorite = CellRendererFavorite(self.tree_view) 138 156 cell_favorite.connect('clicked', self.__favorite_clicked_cb) 139 157 … … class BaseListView(gtk.Bin): 251 269 else: 252 270 cell.props.xo_color = None 253 271 272 def __selected_cb(self, cell, path): 273 from jarabe.journal.journalactivity import get_journal 274 journal = get_journal() 275 276 row = self._model[path] 277 metadata = model.get(row[ListModel.COLUMN_UID]) 278 if metadata.get('selected', '0') == '1': 279 metadata['selected'] = '0' 280 self._selected_entries = self._selected_entries - 1 281 if self._selected_entries < 1: 282 journal.switch_to_editing_mode(False) 283 else: 284 metadata['selected'] = '1' 285 self._selected_entries = self._selected_entries + 1 286 if self._selected_entries > 0: 287 journal.switch_to_editing_mode(True) 288 289 model.write(metadata, update_mtime=False) 290 254 291 def __favorite_clicked_cb(self, cell, path): 255 292 row = self._model[path] 256 293 metadata = model.get(row[ListModel.COLUMN_UID]) … … class BaseListView(gtk.Bin): 274 311 ListModel.COLUMN_TIMESTAMP)) 275 312 self._query = query_dict 276 313 314 # This refresh is always needed, since the query has changed. 277 315 self.refresh() 278 316 279 317 def refresh(self): 318 if not self._inhibit_refresh: 319 self.proceed_with_refresh() 320 321 def proceed_with_refresh(self): 280 322 logging.debug('ListView.refresh query %r', self._query) 281 323 self._stop_progress_bar() 282 324 … … class BaseListView(gtk.Bin): 466 508 self.update_dates() 467 509 return True 468 510 511 def get_model(self): 512 return self._model 513 514 def inhibit_refresh(self, inhibit): 515 self._inhibit_refresh = inhibit 516 469 517 470 518 class ListView(BaseListView): 471 519 __gtype_name__ = 'JournalListView' -
src/jarabe/journal/model.py
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index 5285a7c..39547a4 100644
a b 1 1 # Copyright (C) 2007-2011, One Laptop per Child 2 # Copyright (C) 2012, Walter Bender <walter@sugarlabs.org> 3 # Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org> 4 # Copyright (C) 2012, Martin Abente <tch@sugarlabs.org> 5 # Copyright (C) 2012, Ajay Garg <ajay@activitycentral.com> 2 6 # 3 7 # This program is free software; you can redistribute it and/or modify 4 8 # it under the terms of the GNU General Public License as published by … … from sugar import dispatch 37 41 from sugar import mime 38 42 from sugar import util 39 43 40 41 44 DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' 42 45 DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' 43 46 DS_DBUS_PATH = '/org/laptop/sugar/DataStore' … … DS_DBUS_PATH = '/org/laptop/sugar/DataStore' 45 48 # Properties the journal cares about. 46 49 PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id', 47 50 'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type', 48 'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid'] 51 'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 52 'uid', 'selected'] 49 53 50 54 MIN_PAGES_TO_CACHE = 3 51 55 MAX_PAGES_TO_CACHE = 5 … … def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): 651 655 file_path, 652 656 transfer_ownership) 653 657 else: 658 # HACK: For documents: modify the mount-point 659 from jarabe.journal.journalactivity import get_mount_point 660 if get_mount_point() == get_documents_path(): 661 metadata['mountpoint'] = get_documents_path() 662 654 663 object_id = _write_entry_on_external_device(metadata, file_path) 655 664 656 665 return object_id -
src/jarabe/journal/palettes.py
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 27b0b54..2916a14 100644
a b 1 1 # Copyright (C) 2008 One Laptop Per Child 2 # Copyright (C) 2012, Walter Bender <walter@sugarlabs.org> 3 # Copyright (C) 2012, Gonzalo Odiard <gonzalo@laptop.org> 4 # Copyright (C) 2012, Martin Abente <tch@sugarlabs.org> 5 # Copyright (C) 2012, Ajay Garg <ajay@activitycentral.com> 2 6 # 3 7 # This program is free software; you can redistribute it and/or modify 4 8 # it under the terms of the GNU General Public License as published by … … 15 19 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 20 17 21 from gettext import gettext as _ 22 from gettext import ngettext 18 23 import logging 19 24 import os 20 25 … … import gtk 23 28 import gconf 24 29 import gio 25 30 import glib 31 import time 32 33 from sugar import _sugarext 26 34 27 35 from sugar.graphics import style 28 36 from sugar.graphics.palette import Palette … … from jarabe.journal import model 39 47 40 48 friends_model = friends.get_model() 41 49 50 _copy_menu_helper = None 51 42 52 43 53 class BulkOperationDetails(): 44 54 … … class ObjectPalette(Palette): 129 139 menu_item.set_image(icon) 130 140 self.menu.append(menu_item) 131 141 menu_item.show() 132 copy_menu = CopyMenu(metadata) 142 copy_menu = CopyMenu() 143 copy_menu_helper = get_copy_menu_helper() 144 145 metadata_list = [] 146 metadata_list.append(metadata) 147 copy_menu_helper.insert_copy_to_menu_items(copy_menu, 148 metadata_list, 149 False, 150 False, 151 False) 133 152 copy_menu.connect('volume-error', self.__volume_error_cb) 153 copy_menu_helper.connect('volume-error', self.__volume_error_cb) 134 154 menu_item.set_submenu(copy_menu) 135 155 136 156 if self._metadata['mountpoint'] == '/': … … class CopyMenu(gtk.Menu): 260 280 ([str, str])), 261 281 } 262 282 263 def __init__(self , metadata):283 def __init__(self): 264 284 gobject.GObject.__init__(self) 265 285 266 self._metadata = metadata267 286 268 clipboard_menu = ClipboardMenu(self._metadata) 269 clipboard_menu.set_image(Icon(icon_name='toolbar-edit', 270 icon_size=gtk.ICON_SIZE_MENU)) 271 clipboard_menu.connect('volume-error', self.__volume_error_cb) 272 self.append(clipboard_menu) 273 clipboard_menu.show() 287 class ActionItem(gobject.GObject): 288 """ 289 This class implements the course of actions that happens when clicking 290 upon an Action-Item (eg. Batch-Copy-Toolbar-button; 291 Actual-Batch-Copy-To-Journal-button; 292 Actual-Batch-Copy-To-Documents-button; 293 Actual-Batch-Copy-To-Mounted-Drive-button; 294 Actual-Batch-Copy-To-Clipboard-button; 295 Single-Copy-To-Journal-button; 296 Single-Copy-To-Documents-button; 297 Single-Copy-To-Mounted-Drive-button; 298 Single-Copy-To-Clipboard-button; 299 Batch-Erase-Button; 300 Select-None-Toolbar-button; 301 Select-All-Toolbar-button 302 """ 274 303 275 from jarabe.journal import journalactivity 276 journal_model = journalactivity.get_journal() 277 if journal_model.get_mount_point() != model.get_documents_path(): 278 documents_menu = DocumentsMenu(self._metadata) 279 documents_menu.set_image(Icon(icon_name='user-documents', 280 icon_size=gtk.ICON_SIZE_MENU)) 281 documents_menu.connect('volume-error', self.__volume_error_cb) 282 self.append(documents_menu) 283 documents_menu.show() 304 def __init__(self, label, metadata_list, show_editing_alert, 305 show_progress_info_alert, batch_mode, 306 need_to_popup_options, 307 operate_on_deselected_entries, 308 switch_to_normal_mode_after_completion): 309 gobject.GObject.__init__(self) 284 310 285 if self._metadata['mountpoint'] != '/': 286 client = gconf.client_get_default() 287 color = XoColor(client.get_string('/desktop/sugar/user/color')) 288 journal_menu = VolumeMenu(self._metadata, _('Journal'), '/') 289 journal_menu.set_image(Icon(icon_name='activity-journal', 290 xo_color=color, 291 icon_size=gtk.ICON_SIZE_MENU)) 292 journal_menu.connect('volume-error', self.__volume_error_cb) 293 self.append(journal_menu) 294 journal_menu.show() 311 self._label = label 312 self._metadata_list = metadata_list 313 self._show_progress_info_alert = show_progress_info_alert 314 self._batch_mode = batch_mode 315 self._operate_on_deselected_entries = \ 316 operate_on_deselected_entries 317 self._switch_to_normal_mode_after_completion = \ 318 switch_to_normal_mode_after_completion 295 319 296 volume_monitor = gio.volume_monitor_get() 297 icon_theme = gtk.icon_theme_get_default() 298 for mount in volume_monitor.get_mounts(): 299 if self._metadata['mountpoint'] == mount.get_root().get_path(): 300 continue 301 volume_menu = VolumeMenu(self._metadata, mount.get_name(), 302 mount.get_root().get_path()) 303 for name in mount.get_icon().props.names: 304 if icon_theme.has_icon(name): 305 volume_menu.set_image(Icon(icon_name=name, 306 icon_size=gtk.ICON_SIZE_MENU)) 307 break 308 volume_menu.connect('volume-error', self.__volume_error_cb) 309 self.append(volume_menu) 310 volume_menu.show() 320 actionable_signal = self._get_actionable_signal() 311 321 312 def __volume_error_cb(self, menu_item, message, severity): 313 self.emit('volume-error', message, severity) 322 if need_to_popup_options: 323 self.connect(actionable_signal, self._fill_and_pop_up_options) 324 else: 325 if show_editing_alert: 326 self.connect(actionable_signal, self._show_editing_alert) 327 else: 328 self.connect(actionable_signal, self._pre_operate_per_action) 329 330 def _get_actionable_signal(self): 331 """ 332 Some widgets like 'buttons' have 'clicked' as actionable signal; 333 some like 'menuitems' have 'activate' as actionable signal. 334 """ 335 336 raise NotImplementedError 337 338 def _fill_and_pop_up_options(self): 339 """ 340 Eg. Batch-Copy-Toolbar-button does not do anything by itself 341 useful; but rather pops-up the actual 'copy-to' options. 342 """ 343 344 raise NotImplementedError 345 346 def _show_editing_alert(self, widget_clicked): 347 """ 348 Upon clicking the actual operation button (eg. 349 Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT 350 Batch-Copy-Toolbar-button, since it does not do anything 351 actually useful, but only pops-up the actual 'copy-to' options. 352 """ 353 354 alert_parameters = self._get_editing_alert_parameters() 355 title = alert_parameters[0] 356 message = alert_parameters[1] 357 operation = alert_parameters[2] 358 359 from jarabe.journal.journalactivity import get_journal 360 get_journal().add_editing_alert(None, title, message, operation, 361 self._pre_operate_per_action) 362 363 def _get_editing_alert_parameters(self): 364 """ 365 Get the alert parameters for widgets that can show editing 366 alert. 367 """ 368 369 # For batch-operations, fetch the metadata-list. 370 if self._batch_mode: 371 self._metadata_list = self._get_metadata_list() 372 entries_len = len(self._metadata_list) 373 374 title = self._get_editing_alert_title() 375 message = self._get_editing_alert_message(entries_len) 376 operation = self._get_editing_alert_operation() 377 378 return (title, message, operation) 379 380 def _get_metadata_list(self): 381 """ 382 Get the metadata list, according to button-type. For eg, 383 Select-All-Toolbar-button operates on non-selected entries; 384 while othere operate on selected-entries. 385 """ 386 387 from jarabe.journal.journalactivity import get_journal 388 journal = get_journal() 389 390 if self._operate_on_deselected_entries: 391 return journal.get_metadata_list('0') 392 else: 393 return journal.get_metadata_list('1') 394 395 def _get_editing_alert_title(self): 396 raise NotImplementedError 397 398 def _get_editing_alert_message(self, entries_len): 399 raise NotImplementedError 400 401 def _get_editing_alert_operation(self): 402 raise NotImplementedError 403 404 def _is_metadata_list_empty(self): 405 return (self._metadata_list is None) or \ 406 (len(self._metadata_list) == 0) 407 408 def _pre_operate_per_action(self, obj): 409 """ 410 This is the stage, just before the FIRST metadata gets into its 411 processing cycle. 412 """ 413 414 self._skip_all = False 415 416 # For batch-operations, fetch the metadata list again. 417 if (self._batch_mode): 418 self._metadata_list = self._get_metadata_list() 419 420 # Set the initial length of metadata-list. 421 self._metadata_list_initial_len = len(self._metadata_list) 422 423 # Next, proceed with the metadata 424 self._pre_operate_per_metadata_per_action() 425 426 def _pre_operate_per_metadata_per_action(self): 427 """ 428 This is the stage, just before EVERY metadata gets into doing 429 its actual work. 430 """ 431 432 # If there is still some metadata left, proceed with the 433 # metadata operation. 434 # Else, proceed to post-operations. 435 if len(self._metadata_list) > 0: 436 metadata = self._metadata_list.pop(0) 437 438 # De-select the entry. 439 metadata['selected'] = '0' 440 model.write(metadata, update_mtime=False) 441 442 # If info-alert needs to be shown, show the alert, and 443 # arrange for actual operation. 444 # Else, proceed to actual operation directly. 445 if self._show_progress_info_alert: 446 current_len = len(self._metadata_list) 447 448 # TRANS: Do not translate the two %d, and the %s. 449 info_alert_message = _('( %d / %d ) %s') % ( 450 self._metadata_list_initial_len - current_len, 451 self._metadata_list_initial_len, metadata['title']) 452 453 from jarabe.journal.journalactivity import get_journal 454 get_journal().add_info_alert(None, 455 self._get_info_alert_title() + ' ...', 456 info_alert_message, False, 457 self._operate_per_metadata_per_action, 458 metadata) 459 else: 460 self._operate_per_metadata_per_action(metadata) 461 else: 462 self._post_operate_per_action() 463 464 def _get_info_alert_title(self): 465 raise NotImplementedError 466 467 def _operate_per_metadata_per_action(self, metadata): 468 """ 469 This is just a code-convenient-function, which allows 470 runtime-overriding. It just delegates to the actual 471 "self._operate" method, the actual which is determined at 472 runtime. 473 """ 474 475 # Pass the callback for the post-operation-for-metadata. This 476 # will ensure that async-operations on the metadata are taken 477 # care of. 478 self._operate(metadata) 479 480 def _operate(self, metadata): 481 """ 482 Actual, core, productive stage for EVERY metadata. 483 """ 484 485 raise NotImplementedError 486 487 def _post_operate_per_metadata_per_action(self, metadata): 488 """ 489 This is the stage, just after EVERY metadata has been 490 processed. 491 """ 492 493 from jarabe.journal.journalactivity import get_journal 494 get_journal().remove_info_alert() 495 496 # Call the next ... 497 self._pre_operate_per_metadata_per_action() 498 499 def _post_operate_per_action(self): 500 """ 501 This is the stage, just after the LAST metadata has been 502 processed. 503 """ 504 505 # Switch to non-editing mode (if applicable), after the operation is complete 506 if self._switch_to_normal_mode_after_completion: 507 from jarabe.journal.journalactivity import get_journal 508 get_journal().switch_to_editing_mode(False) 509 510 def _inhibit_refresh(self, inhibit): 511 from jarabe.journal.journalactivity import get_journal 512 get_journal().get_list_view().inhibit_refresh(inhibit) 513 514 def _refresh(self): 515 from jarabe.journal.journalactivity import get_journal 516 get_journal().get_list_view().refresh() 517 518 def _handle_error_alert(self, error_message, metadata): 519 """ 520 This handles any error scenarios. Examples are of entries that 521 display the message "Entries without a file cannot be copied." 522 This is kind of controller-functionl the model-function is 523 "self._set_error_info_alert". 524 """ 525 526 if self._skip_all: 527 self._post_operate_per_metadata_per_action(metadata) 528 else: 529 self._set_error_info_alert(error_message, metadata) 530 531 def _set_error_info_alert(self, error_message, metadata): 532 """ 533 This method displays the error alert. 534 """ 535 536 current_len = len(self._metadata_list) 314 537 538 # TRANS: Do not translate the two %d, and the three %s. 539 info_alert_message = _('( %d / %d ) Error while %s %s : %s') % ( 540 self._metadata_list_initial_len - current_len, 541 self._metadata_list_initial_len, 542 self._get_info_alert_title(), 543 metadata['title'], 544 error_message) 315 545 316 class VolumeMenu(MenuItem): 317 __gtype_name__ = 'JournalVolumeMenu' 546 from jarabe.journal.journalactivity import get_journal 547 get_journal().add_info_alert(None, 548 self._get_info_alert_title() + ' ...', 549 info_alert_message, True, 550 self._process_error_skipping, 551 metadata) 318 552 553 def _process_error_skipping(self, skip_all, metadata): 554 if skip_all: 555 self._skip_all = True 556 557 # The operation for the current metadata is finished (kinda 558 # pseudo ...) 559 self._post_operate_per_metadata_per_action(metadata) 560 561 562 class BaseCopyMenuItem(MenuItem, ActionItem): 319 563 __gsignals__ = { 320 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,321 ([str, str])),322 }564 'volume-error': (gobject.SIGNAL_RUN_FIRST, 565 gobject.TYPE_NONE, ([str, str])), 566 } 323 567 324 def __init__(self, metadata, label, mount_point): 568 569 def __init__(self, metadata_list, label, show_editing_alert, 570 show_progress_info_alert, batch_mode): 325 571 MenuItem.__init__(self, label) 326 self._metadata = metadata 327 self.connect('activate', self.__copy_to_volume_cb, mount_point) 572 ActionItem.__init__(self, label, metadata_list, show_editing_alert, 573 show_progress_info_alert, batch_mode, 574 need_to_popup_options=False, 575 operate_on_deselected_entries=False, 576 switch_to_normal_mode_after_completion=True) 328 577 329 def __copy_to_volume_cb(self, menu_item, mount_point): 330 file_path = model.get_file(self._metadata['uid']) 578 def _get_actionable_signal(self): 579 return 'activate' 580 581 def _get_editing_alert_title(self): 582 return _('Copy') 583 584 def _get_editing_alert_message(self, entries_len): 585 return ngettext('Do you want to copy %d entry to %s?', 586 'Do you want to copy %d entries to %s?', 587 entries_len) % (entries_len, self._label) 588 589 def _get_editing_alert_operation(self): 590 return _('Copy') 591 592 def _get_info_alert_title(self): 593 return _('Copying') 594 595 596 class VolumeMenu(BaseCopyMenuItem): 597 def __init__(self, metadata_list, label, mount_point, 598 show_editing_alert, show_progress_info_alert, 599 batch_mode): 600 BaseCopyMenuItem.__init__(self, metadata_list, label, 601 show_editing_alert, 602 show_progress_info_alert, batch_mode) 603 self._mount_point = mount_point 604 605 def _operate(self, metadata): 606 file_path = model.get_file(metadata['uid']) 331 607 332 608 if not file_path or not os.path.exists(file_path): 333 609 logging.warn('Entries without a file cannot be copied.') 334 self.emit('volume-error', 335 _('Entries without a file cannot be copied.'), 336 _('Warning')) 610 error_message = _('Entries without a file cannot be copied.') 611 if self._batch_mode: 612 self._handle_error_alert(error_message, metadata) 613 else: 614 self.emit('volume-error', error_message, _('Warning')) 337 615 return 338 616 339 617 try: 340 model.copy( self._metadata,mount_point)618 model.copy(metadata, self._mount_point) 341 619 except IOError, e: 342 620 logging.exception('Error while copying the entry. %s', e.strerror) 343 self.emit('volume-error', 344 _('Error while copying the entry. %s') % e.strerror, 345 _('Error')) 346 347 348 class ClipboardMenu(MenuItem): 349 __gtype_name__ = 'JournalClipboardMenu' 621 error_message = _('Error while copying the entry. %s') % e.strerror 622 if self._batch_mode: 623 self._handle_error_alert(error_message, metadata) 624 else: 625 self.emit('volume-error', error_message, _('Error')) 626 return 350 627 351 __gsignals__ = { 352 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, 353 ([str, str])), 354 } 628 # This is sync-operation. Thus, call the callback. 629 self._post_operate_per_metadata_per_action(metadata) 355 630 356 def __init__(self, metadata):357 MenuItem.__init__(self, _('Clipboard'))358 631 359 self._temp_file_path = None 360 self._metadata = metadata 361 self.connect('activate', self.__copy_to_clipboard_cb) 632 class ClipboardMenu(BaseCopyMenuItem): 633 def __init__(self, metadata_list, show_editing_alert, 634 show_progress_info_alert, batch_mode): 635 BaseCopyMenuItem.__init__(self, metadata_list, _('Clipboard'), 636 show_editing_alert, 637 show_progress_info_alert, 638 batch_mode) 639 self._temp_file_path_list = [] 362 640 363 def _ _copy_to_clipboard_cb(self, menu_item):364 file_path = model.get_file( self._metadata['uid'])641 def _operate(self, metadata): 642 file_path = model.get_file(metadata['uid']) 365 643 if not file_path or not os.path.exists(file_path): 366 644 logging.warn('Entries without a file cannot be copied.') 367 self.emit('volume-error', 368 _('Entries without a file cannot be copied.'), 369 _('Warning')) 645 error_message = _('Entries without a file cannot be copied.') 646 if self._batch_mode: 647 self._handle_error_alert(error_message, metadata) 648 else: 649 self.emit('volume-error', error_message, _('Warning')) 370 650 return 371 651 372 652 clipboard = gtk.Clipboard() 373 653 clipboard.set_with_data([('text/uri-list', 0, 0)], 374 654 self.__clipboard_get_func_cb, 375 self.__clipboard_clear_func_cb) 655 self.__clipboard_clear_func_cb, 656 metadata) 376 657 377 def __clipboard_get_func_cb(self, clipboard, selection_data, info, data): 658 def __clipboard_get_func_cb(self, clipboard, selection_data, info, 659 metadata): 378 660 # Get hold of a reference so the temp file doesn't get deleted 379 self._temp_file_path = model.get_file( self._metadata['uid'])661 self._temp_file_path = model.get_file(metadata['uid']) 380 662 logging.debug('__clipboard_get_func_cb %r', self._temp_file_path) 381 663 selection_data.set_uris(['file://' + self._temp_file_path]) 382 664 383 def __clipboard_clear_func_cb(self, clipboard, data):665 def __clipboard_clear_func_cb(self, clipboard, metadata): 384 666 # Release and delete the temp file 385 667 self._temp_file_path = None 386 668 669 # This is async-operation; and this is the ending point. 670 self._post_operate_per_metadata_per_action(metadata) 387 671 388 class DocumentsMenu(MenuItem):389 __gtype_name__ = 'JournalDocumentsMenu'390 391 __gsignals__ = {392 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,393 ([str, str])),394 }395 396 def __init__(self, metadata):397 MenuItem.__init__(self, _('Documents'))398 672 399 self._temp_file_path = None 400 self._metadata = metadata 401 self.connect('activate', self.__copy_to_documents_cb) 673 class DocumentsMenu(BaseCopyMenuItem): 674 def __init__(self, metadata_list, show_editing_alert, 675 show_progress_info_alert, batch_mode): 676 BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'), 677 show_editing_alert, 678 show_progress_info_alert, 679 batch_mode) 402 680 403 def _ _copy_to_documents_cb(self, menu_item):404 file_path = model.get_file( self._metadata['uid'])681 def _operate(self, metadata): 682 file_path = model.get_file(metadata['uid']) 405 683 if not file_path or not os.path.exists(file_path): 406 684 logging.warn('Entries without a file cannot be copied.') 407 self.emit('volume-error', 408 _('Entries without a file cannot be copied.'), 409 _('Warning')) 685 error_message = _('Entries without a file cannot be copied.') 686 if self._batch_mode: 687 self._handle_error_alert(error_message, metadata) 688 else: 689 self.emit('volume-error', error_message, _('Warning')) 410 690 return 411 691 412 model.copy(self._metadata, model.get_documents_path()) 692 model.copy(metadata, model.get_documents_path()) 693 694 # This is sync-operation. Call the post-operation now. 695 self._post_operate_per_metadata_per_action(metadata) 413 696 414 697 415 698 class GroupsMenu(gtk.Menu): … … class BuddyPalette(Palette): 538 821 icon=buddy_icon) 539 822 540 823 # TODO: Support actions on buddies, like make friend, invite, etc. 824 825 826 827 class CopyMenuHelper(gtk.Menu): 828 __gtype_name__ = 'JournalCopyMenuHelper' 829 830 __gsignals__ = { 831 'volume-error': (gobject.SIGNAL_RUN_FIRST, 832 gobject.TYPE_NONE, 833 ([str, str])), 834 } 835 836 def insert_copy_to_menu_items(self, menu, metadata_list, 837 show_editing_alert, 838 show_progress_info_alert, 839 batch_mode): 840 self._metadata_list = metadata_list 841 842 clipboard_menu = ClipboardMenu(metadata_list, 843 show_editing_alert, 844 show_progress_info_alert, 845 batch_mode) 846 clipboard_menu.set_image(Icon(icon_name='toolbar-edit', 847 icon_size=gtk.ICON_SIZE_MENU)) 848 clipboard_menu.connect('volume-error', self.__volume_error_cb) 849 menu.append(clipboard_menu) 850 clipboard_menu.show() 851 852 from jarabe.journal.journalactivity import get_mount_point 853 854 if get_mount_point() != model.get_documents_path(): 855 documents_menu = DocumentsMenu(metadata_list, 856 show_editing_alert, 857 show_progress_info_alert, 858 batch_mode) 859 documents_menu.set_image(Icon(icon_name='user-documents', 860 icon_size=gtk.ICON_SIZE_MENU)) 861 documents_menu.connect('volume-error', self.__volume_error_cb) 862 menu.append(documents_menu) 863 documents_menu.show() 864 865 if get_mount_point() != '/': 866 client = gconf.client_get_default() 867 color = XoColor(client.get_string('/desktop/sugar/user/color')) 868 journal_menu = VolumeMenu(metadata_list, _('Journal'), '/', 869 show_editing_alert, 870 show_progress_info_alert, 871 batch_mode) 872 journal_menu.set_image(Icon(icon_name='activity-journal', 873 xo_color=color, 874 icon_size=gtk.ICON_SIZE_MENU)) 875 journal_menu.connect('volume-error', self.__volume_error_cb) 876 menu.append(journal_menu) 877 journal_menu.show() 878 879 volume_monitor = gio.volume_monitor_get() 880 icon_theme = gtk.icon_theme_get_default() 881 for mount in volume_monitor.get_mounts(): 882 if get_mount_point() == mount.get_root().get_path(): 883 continue 884 885 volume_menu = VolumeMenu(metadata_list, mount.get_name(), 886 mount.get_root().get_path(), 887 show_editing_alert, 888 show_progress_info_alert, 889 batch_mode) 890 for name in mount.get_icon().props.names: 891 if icon_theme.has_icon(name): 892 volume_menu.set_image(Icon(icon_name=name, 893 icon_size=gtk.ICON_SIZE_MENU)) 894 break 895 896 volume_menu.connect('volume-error', self.__volume_error_cb) 897 menu.insert(volume_menu, -1) 898 volume_menu.show() 899 900 def __volume_error_cb(self, menu_item, message, severity): 901 self.emit('volume-error', message, severity) 902 903 904 def get_copy_menu_helper(): 905 global _copy_menu_helper 906 if _copy_menu_helper is None: 907 _copy_menu_helper = CopyMenuHelper() 908 return _copy_menu_helper