[Sugar-devel] [PATCH sugar] Add duplicate functionality to the Journal and enhance copy functionality

Simon Schampijer simon at schampijer.de
Fri Jul 29 12:35:41 EDT 2011


This patch adds a duplicate option to the Journal entry palette and
the entry detail view. This will replace the keep button from the
activity toolbar.

The copy option which copied previously to the clipboard by default
has been enhanced to allow: copying to the clipboard, copying to an
external device, copying from an external device to the Journal and
copying between external devices. Copying to the clipboard is now
a visible option in the menu. In the detail view the palette pops up
when doing a left click and showing the available options instead of
copying directly to the clipboard.

The design discussion has been taking place
at: http://lists.sugarlabs.org/archive/sugar-devel/2011-May/031316.html

Signed-off-by: Simon Schampijer <simon at laptop.org>
---
 src/jarabe/journal/journalactivity.py |    1 +
 src/jarabe/journal/journaltoolbox.py  |  112 +++++++++++----------
 src/jarabe/journal/listview.py        |   13 +++
 src/jarabe/journal/model.py           |    2 +
 src/jarabe/journal/palettes.py        |  174 +++++++++++++++++++++++++++++----
 5 files changed, 227 insertions(+), 75 deletions(-)

diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index a33038a..bb1c7f6 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -171,6 +171,7 @@ class JournalActivity(JournalWindow):
         self._list_view = ListView()
         self._list_view.connect('detail-clicked', self.__detail_clicked_cb)
         self._list_view.connect('clear-clicked', self.__clear_clicked_cb)
+        self._list_view.connect('volume-error', self.__volume_error_cb)
         self._main_view.pack_start(self._list_view)
         self._list_view.show()
 
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index d825bc9..82d327e 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -26,6 +26,7 @@ import gobject
 import gio
 import gtk
 
+from sugar.graphics.palette import Palette
 from sugar.graphics.toolbox import Toolbox
 from sugar.graphics.toolcombobox import ToolComboBox
 from sugar.graphics.toolbutton import ToolButton
@@ -37,11 +38,12 @@ from sugar.graphics.xocolor import XoColor
 from sugar.graphics import iconentry
 from sugar.graphics import style
 from sugar import mime
-from sugar import profile
 
 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
 
 
 _AUTOSEARCH_TIMEOUT = 1000
@@ -370,19 +372,24 @@ class EntryToolbar(gtk.Toolbar):
         self.add(self._resume)
         self._resume.show()
 
-        self._copy = ToolButton()
-
         client = gconf.client_get_default()
         color = XoColor(client.get_string('/desktop/sugar/user/color'))
+        self._copy = ToolButton()
         icon = Icon(icon_name='edit-copy', xo_color=color)
         self._copy.set_icon_widget(icon)
         icon.show()
 
-        self._copy.set_tooltip(_('Copy'))
+        self._copy.set_tooltip(_('Copy to'))
         self._copy.connect('clicked', self._copy_clicked_cb)
         self.add(self._copy)
         self._copy.show()
 
+        self._duplicate = ToolButton()
+        icon = Icon(icon_name='edit-duplicate', xo_color=color)
+        self._duplicate.set_icon_widget(icon)
+        self._duplicate.set_tooltip(_('Duplicate'))
+        self.add(self._duplicate)
+
         separator = gtk.SeparatorToolItem()
         self.add(separator)
         separator.show()
@@ -395,6 +402,16 @@ class EntryToolbar(gtk.Toolbar):
 
     def set_metadata(self, metadata):
         self._metadata = metadata
+        color = misc.get_icon_color(self._metadata)
+        self._copy.get_icon_widget().props.xo_color = color
+        if self._metadata['mountpoint'] == '/':
+            self._duplicate.connect('clicked', self._duplicate_clicked_cb)
+            self._duplicate.show()
+            icon = self._duplicate.get_icon_widget()
+            icon.props.xo_color = color
+            icon.show()
+        else:
+            self._duplicate.hide()
         self._refresh_copy_palette()
         self._refresh_resume_palette()
 
@@ -402,19 +419,17 @@ class EntryToolbar(gtk.Toolbar):
         misc.resume(self._metadata)
 
     def _copy_clicked_cb(self, button):
-        clipboard = gtk.Clipboard()
-        clipboard.set_with_data([('text/uri-list', 0, 0)],
-                                self.__clipboard_get_func_cb,
-                                self.__clipboard_clear_func_cb)
-
-    def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
-        # Get hold of a reference so the temp file doesn't get deleted
-        self._temp_file_path = model.get_file(self._metadata['uid'])
-        selection_data.set_uris(['file://' + self._temp_file_path])
-
-    def __clipboard_clear_func_cb(self, clipboard, data):
-        # Release and delete the temp file
-        self._temp_file_path = None
+        button.palette.popup(immediate=True, state=Palette.SECONDARY)
+
+    def _duplicate_clicked_cb(self, button):
+        file_path = model.get_file(self._metadata['uid'])
+        try:
+            model.copy(self._metadata, '/')
+        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'))
 
     def _erase_button_clicked_cb(self, button):
         registry = bundleregistry.get_registry()
@@ -427,24 +442,6 @@ class EntryToolbar(gtk.Toolbar):
     def _resume_menu_item_activate_cb(self, menu_item, service_name):
         misc.resume(self._metadata, service_name)
 
-    def _copy_menu_item_activate_cb(self, menu_item, mount_point):
-        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
-
-        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'))
-
     def _refresh_copy_palette(self):
         palette = self._copy.get_palette()
 
@@ -452,35 +449,42 @@ 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'] != '/':
-            journal_item = MenuItem(_('Journal'))
-            journal_item.set_image(Icon(
-                    icon_name='activity-journal',
-                    xo_color=profile.get_color(),
-                    icon_size=gtk.ICON_SIZE_MENU))
-            journal_item.connect('activate',
-                    self._copy_menu_item_activate_cb, '/')
-            journal_item.show()
-            palette.menu.append(journal_item)
+            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
-            menu_item = MenuItem(mount.get_name())
-
-            icon_theme = gtk.icon_theme_get_default()
+            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):
-                    menu_item.set_image(Icon(icon_name=name,
-                                             icon_size=gtk.ICON_SIZE_MENU))
+                    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()
 
-            menu_item.connect('activate',
-                              self._copy_menu_item_activate_cb,
-                              mount.get_root().get_path())
-            palette.menu.append(menu_item)
-            menu_item.show()
+    def __volume_error_cb(self, menu_item, message, severity):
+        self.emit('volume-error', message, severity)
 
     def _refresh_resume_palette(self):
         if self._metadata.get('activity_id', ''):
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index 70ab701..a9f5a53 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -476,6 +476,8 @@ class ListView(BaseListView):
     __gsignals__ = {
         'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                            ([object])),
+        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                         ([str, str])),
     }
 
     def __init__(self):
@@ -491,6 +493,7 @@ class ListView(BaseListView):
 
         self.cell_icon.connect('clicked', self.__icon_clicked_cb)
         self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb)
+        self.cell_icon.connect('volume-error', self.__volume_error_cb)
 
         cell_detail = CellRendererDetail(self.tree_view)
         cell_detail.connect('clicked', self.__detail_cell_clicked_cb)
@@ -532,6 +535,9 @@ class ListView(BaseListView):
     def __detail_clicked_cb(self, cell, uid):
         self.emit('detail-clicked', uid)
 
+    def __volume_error_cb(self, cell, message, severity):
+        self.emit('volume-error', message, severity)
+
     def __icon_clicked_cb(self, cell, path):
         row = self.tree_view.get_model()[path]
         metadata = model.get(row[ListModel.COLUMN_UID])
@@ -586,6 +592,8 @@ class CellRendererActivityIcon(CellRendererIcon):
     __gsignals__ = {
         'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                            ([str])),
+        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                           ([str, str])),
     }
 
     def __init__(self, tree_view):
@@ -610,11 +618,16 @@ class CellRendererActivityIcon(CellRendererIcon):
         palette = ObjectPalette(metadata, detail=True)
         palette.connect('detail-clicked',
                         self.__detail_clicked_cb)
+        palette.connect('volume-error',
+                        self.__volume_error_cb)
         return palette
 
     def __detail_clicked_cb(self, palette, uid):
         self.emit('detail-clicked', uid)
 
+    def __volume_error_cb(self, palette, message, severity):
+        self.emit('volume-error', message, severity)
+
     def set_show_palette(self, show_palette):
         self._show_palette = show_palette
 
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 4ea6b7e..ddf9c07 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -621,6 +621,8 @@ def copy(metadata, mount_point):
         client = gconf.client_get_default()
         metadata['icon-color'] = client.get_string('/desktop/sugar/user/color')
     file_path = get_file(metadata['uid'])
+    if file_path is None:
+        file_path = ''
 
     metadata['mountpoint'] = mount_point
     del metadata['uid']
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 7091378..0812475 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -21,6 +21,7 @@ import os
 import gobject
 import gtk
 import gconf
+import gio
 
 from sugar.graphics import style
 from sugar.graphics.palette import Palette
@@ -43,16 +44,18 @@ class ObjectPalette(Palette):
     __gsignals__ = {
         'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                            ([str])),
+        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                         ([str, str])),
     }
 
     def __init__(self, metadata, detail=False):
 
         self._metadata = metadata
-        self._temp_file_path = None
 
         activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
         activity_icon.props.file = misc.get_icon_name(metadata)
-        activity_icon.props.xo_color = misc.get_icon_color(metadata)
+        color = misc.get_icon_color(metadata)
+        activity_icon.props.xo_color = color
 
         if 'title' in metadata:
             title = gobject.markup_escape_text(metadata['title'])
@@ -86,15 +89,24 @@ class ObjectPalette(Palette):
             self.menu.append(menu_item)
             menu_item.show()
 
-        client = gconf.client_get_default()
-        color = XoColor(client.get_string('/desktop/sugar/user/color'))
-        menu_item = MenuItem(_('Copy'))
+        menu_item = MenuItem(_('Copy to'))
         icon = Icon(icon_name='edit-copy', xo_color=color,
                     icon_size=gtk.ICON_SIZE_MENU)
         menu_item.set_image(icon)
-        menu_item.connect('activate', self.__copy_activate_cb)
         self.menu.append(menu_item)
         menu_item.show()
+        copy_menu = CopyMenu(metadata)
+        copy_menu.connect('volume-error', self.__volume_error_cb)
+        menu_item.set_submenu(copy_menu)
+
+        if self._metadata['mountpoint'] == '/':
+            menu_item = MenuItem(_('Duplicate'))
+            icon = Icon(icon_name='edit-duplicate', xo_color=color,
+                        icon_size=gtk.ICON_SIZE_MENU)
+            menu_item.set_image(icon)
+            menu_item.connect('activate', self.__duplicate_activate_cb)
+            self.menu.append(menu_item)
+            menu_item.show()
 
         menu_item = MenuItem(_('Send to'), 'document-send')
         self.menu.append(menu_item)
@@ -118,21 +130,15 @@ class ObjectPalette(Palette):
     def __start_activate_cb(self, menu_item):
         misc.resume(self._metadata)
 
-    def __copy_activate_cb(self, menu_item):
-        clipboard = gtk.Clipboard()
-        clipboard.set_with_data([('text/uri-list', 0, 0)],
-                                self.__clipboard_get_func_cb,
-                                self.__clipboard_clear_func_cb)
-
-    def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
-        # Get hold of a reference so the temp file doesn't get deleted
-        self._temp_file_path = model.get_file(self._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):
-        # Release and delete the temp file
-        self._temp_file_path = None
+    def __duplicate_activate_cb(self, menu_item):
+        file_path = model.get_file(self._metadata['uid'])
+        try:
+            model.copy(self._metadata, '/')
+        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'))
 
     def __erase_activate_cb(self, menu_item):
         model.delete(self._metadata['uid'])
@@ -140,6 +146,9 @@ class ObjectPalette(Palette):
     def __detail_activate_cb(self, menu_item):
         self.emit('detail-clicked', self._metadata['uid'])
 
+    def __volume_error_cb(self, menu_item, message, severity):
+        self.emit('volume-error', message, severity)
+
     def __friend_selected_cb(self, menu_item, buddy):
         logging.debug('__friend_selected_cb')
         file_name = model.get_file(self._metadata['uid'])
@@ -162,6 +171,129 @@ class ObjectPalette(Palette):
                                     mime_type)
 
 
+class CopyMenu(gtk.Menu):
+    __gtype_name__ = 'JournalCopyMenu'
+
+    __gsignals__ = {
+        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                         ([str, str])),
+    }
+
+    def __init__(self, metadata):
+        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()
+
+        if self._metadata['mountpoint'] != '/':
+            client = gconf.client_get_default()
+            color = XoColor(client.get_string('/desktop/sugar/user/color'))
+            journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
+            journal_menu.set_image(Icon(icon_name='activity-journal',
+                                        xo_color=color,
+                                        icon_size=gtk.ICON_SIZE_MENU))
+            journal_menu.connect('volume-error', self.__volume_error_cb)
+            self.append(journal_menu)
+            journal_menu.show()
+
+        volume_monitor = gio.volume_monitor_get()
+        icon_theme = gtk.icon_theme_get_default()
+        for mount in volume_monitor.get_mounts():
+            if self._metadata['mountpoint'] == mount.get_root().get_path():
+                continue
+            volume_menu = VolumeMenu(self._metadata, mount.get_name(),
+                                   mount.get_root().get_path())
+            for name in mount.get_icon().props.names:
+                if icon_theme.has_icon(name):
+                    volume_menu.set_image(Icon(icon_name=name,
+                                               icon_size=gtk.ICON_SIZE_MENU))
+                    break
+            volume_menu.connect('volume-error', self.__volume_error_cb)
+            self.append(volume_menu)
+            volume_menu.show()
+
+    def __volume_error_cb(self, menu_item, message, severity):
+        self.emit('volume-error', message, severity)
+
+
+class VolumeMenu(MenuItem):
+    __gtype_name__ = 'JournalVolumeMenu'
+
+    __gsignals__ = {
+        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                         ([str, str])),
+    }
+
+    def __init__(self, metadata, label, mount_point):
+        MenuItem.__init__(self, label)
+        self._metadata = metadata
+        self.connect('activate', self.__copy_to_volume_cb, mount_point)
+
+    def __copy_to_volume_cb(self, menu_item, mount_point):
+        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
+
+        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'))
+
+
+class ClipboardMenu(MenuItem):
+    __gtype_name__ = 'JournalClipboardMenu'
+
+    __gsignals__ = {
+        'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                         ([str, str])),
+    }
+
+    def __init__(self, metadata):
+        MenuItem.__init__(self, _('Clipboard'))
+
+        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
+
+        clipboard = gtk.Clipboard()
+        clipboard.set_with_data([('text/uri-list', 0, 0)],
+                                self.__clipboard_get_func_cb,
+                                self.__clipboard_clear_func_cb)
+
+    def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+        # Get hold of a reference so the temp file doesn't get deleted
+        self._temp_file_path = model.get_file(self._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):
+        # Release and delete the temp file
+        self._temp_file_path = None
+
+
 class FriendsMenu(gtk.Menu):
     __gtype_name__ = 'JournalFriendsMenu'
 
-- 
1.7.4.4



More information about the Sugar-devel mailing list