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

Simon Schampijer simon at schampijer.de
Mon Aug 15 13:57:44 EDT 2011


Pushed as: 
http://git.sugarlabs.org/sugar/mainline/commit/2148b47c2a5910eeb14fe827059585bef09fc111

Thanks for everyone involved who made this happen.

Regards,
    Simon

On 08/12/2011 08:01 PM, Simon Schampijer wrote:
> This patch adds a duplicate option to the Journal entry palette and
> the entry detail view. This will replace the keep button functionality
> from the activity toolbar. The keep button will be deprecated in another
> patch for the toolkit.
>
> 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  |  116 ++++++++++++----------
>   src/jarabe/journal/listview.py        |   13 +++
>   src/jarabe/journal/model.py           |    2 +
>   src/jarabe/journal/palettes.py        |  174 +++++++++++++++++++++++++++++----
>   5 files changed, 230 insertions(+), 76 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..77334b4 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,23 @@ 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._copy.set_icon_widget(icon)
> +        self._duplicate.set_tooltip(_('Duplicate'))
> +        self.add(self._duplicate)
> +
>           separator = gtk.SeparatorToolItem()
>           self.add(separator)
>           separator.show()
> @@ -396,25 +402,24 @@ class EntryToolbar(gtk.Toolbar):
>       def set_metadata(self, metadata):
>           self._metadata = metadata
>           self._refresh_copy_palette()
> +        self._refresh_duplicate_palette()
>           self._refresh_resume_palette()
>
>       def _resume_clicked_cb(self, button):
>           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.')
> +            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 +432,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 +439,54 @@ 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()
> +
> +    def _refresh_duplicate_palette(self):
> +        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()
>
> -            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'
>



More information about the Sugar-devel mailing list