Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulio Daniel Reyes <nemesiscodex@gmail.com>2013-06-26 16:03:38 (GMT)
committer Julio Daniel Reyes <nemesiscodex@gmail.com>2013-06-26 16:03:38 (GMT)
commit01ef033a6387effb16ba72c580b0387068798f09 (patch)
treeee3bc641e16ef99ad9e1b2b1f88df47880f5f862
parent27841bf56916544943ad0db0df87982cccb8dc38 (diff)
Added multi-select journal
-rw-r--r--data/icons/select-all.svg80
-rw-r--r--data/icons/select-none.svg72
-rw-r--r--src/jarabe/journal/journalactivity.py132
-rw-r--r--src/jarabe/journal/journaltoolbox.py176
-rw-r--r--src/jarabe/journal/listmodel.py59
-rw-r--r--src/jarabe/journal/listview.py53
-rw-r--r--src/jarabe/journal/model.py57
7 files changed, 617 insertions, 12 deletions
diff --git a/data/icons/select-all.svg b/data/icons/select-all.svg
new file mode 100644
index 0000000..f1c29a9
--- /dev/null
+++ b/data/icons/select-all.svg
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 54.696"
+ height="54.696px"
+ version="1.1"
+ viewBox="0 0 55 54.696"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="select-all.svg"><metadata
+ id="metadata13"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs11"><linearGradient
+ id="linearGradient5208"
+ osb:paint="solid"><stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop5210" /></linearGradient><linearGradient
+ id="linearGradient3758"
+ osb:paint="solid"><stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3760" /></linearGradient></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1366"
+ inkscape:window-height="693"
+ id="namedview9"
+ showgrid="false"
+ inkscape:zoom="5.4300132"
+ inkscape:cx="21.684358"
+ inkscape:cy="27.348"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" /><rect
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect5214"
+ width="36.219181"
+ height="42.29937"
+ x="9.3664637"
+ y="6.4843678" /><rect
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect5216"
+ width="9.9359331"
+ height="9.7244711"
+ x="14.04011"
+ y="13.671784" /><rect
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect5216-4"
+ width="9.9359331"
+ height="9.7244711"
+ x="14.10379"
+ y="32.684673" /><path
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 29.44008,18.968646 3.683233,3.683232 6.629818,-9.20808"
+ id="path5298"
+ inkscape:connector-curvature="0" /><path
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 29.834181,38.83604 3.683233,3.683232 6.629818,-9.20808"
+ id="path5298-0"
+ inkscape:connector-curvature="0" /></svg>
diff --git a/data/icons/select-none.svg b/data/icons/select-none.svg
new file mode 100644
index 0000000..0427384
--- /dev/null
+++ b/data/icons/select-none.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 54.696"
+ height="54.696px"
+ version="1.1"
+ viewBox="0 0 55 54.696"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="select-all.svg"><metadata
+ id="metadata13"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs11"><linearGradient
+ id="linearGradient5208"
+ osb:paint="solid"><stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop5210" /></linearGradient><linearGradient
+ id="linearGradient3758"
+ osb:paint="solid"><stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3760" /></linearGradient></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1366"
+ inkscape:window-height="693"
+ id="namedview9"
+ showgrid="false"
+ inkscape:zoom="5.4300132"
+ inkscape:cx="-2.978747"
+ inkscape:cy="27.348"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" /><rect
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect5214"
+ width="36.219181"
+ height="42.29937"
+ x="9.3664637"
+ y="6.4843678" /><rect
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect5216"
+ width="9.9359331"
+ height="9.7244711"
+ x="14.04011"
+ y="13.671784" /><rect
+ style="fill:none;stroke:#ffffff;stroke-width:3.0;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect5216-4"
+ width="9.9359331"
+ height="9.7244711"
+ x="14.10379"
+ y="32.684673" /></svg>
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index bb1c7f6..44a2029 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -17,7 +17,9 @@
import logging
from gettext import gettext as _
+from gettext import ngettext
import uuid
+import gobject
import gtk
import dbus
@@ -25,7 +27,8 @@ import statvfs
import os
from sugar.graphics.window import Window
-from sugar.graphics.alert import ErrorAlert
+from sugar.graphics.alert import Alert, ErrorAlert
+from sugar.graphics.icon import Icon
from sugar.bundle.bundle import ZipExtractException, RegistrationException
from sugar import env
@@ -34,6 +37,7 @@ from sugar import wm
from jarabe.model import bundleregistry
from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.journaltoolbox import EditToolbox
from jarabe.journal.listview import ListView
from jarabe.journal.detailview import DetailView
from jarabe.journal.volumestoolbar import VolumesToolbar
@@ -118,9 +122,12 @@ class JournalActivity(JournalWindow):
self._secondary_view = None
self._list_view = None
self._detail_view = None
+ self._edit_toolbox = None
self._main_toolbox = None
self._detail_toolbox = None
self._volumes_toolbar = None
+ self._editing_mode = False
+ self._editing_alert = None
self._setup_main_view()
self._setup_secondary_view()
@@ -141,6 +148,7 @@ class JournalActivity(JournalWindow):
self._dbus_service = JournalActivityDBusService(self)
self.iconify()
+ self._iconified = True
self._critical_space_alert = None
self._check_available_space()
@@ -172,6 +180,7 @@ class JournalActivity(JournalWindow):
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._list_view.connect('select-toggled', self.__select_toggled_cb)
self._main_view.pack_start(self._list_view)
self._list_view.show()
@@ -185,6 +194,14 @@ class JournalActivity(JournalWindow):
search_toolbar.connect('query-changed', self._query_changed_cb)
search_toolbar.set_mount_point('/')
+ self._edit_toolbox = EditToolbox()
+ edit_toolbar = self._edit_toolbox.edit_toolbar
+ edit_toolbar.connect('edit-none', self.__edit_none_activated_cb)
+ edit_toolbar.connect('edit-all', self.__edit_all_activated_cb)
+ edit_toolbar.connect('edit-erase', self.__edit_erase_activated_cb)
+ edit_toolbar.connect('edit-copy', self.__edit_copy_activated_cb)
+
+
def _setup_secondary_view(self):
self._secondary_view = gtk.VBox()
@@ -197,6 +214,85 @@ class JournalActivity(JournalWindow):
self._secondary_view.pack_end(self._detail_view)
self._detail_view.show()
+ def __edit_none_activated_cb(self, toolbar):
+ list_model = self._list_view.get_model()
+ list_model.set_selection_none()
+
+ def __edit_all_activated_cb(self, toolbar):
+ list_model = self._list_view.get_model()
+ list_model.set_selection_all()
+
+ def _remove_editing_alert(self):
+ if self._editing_alert is not None:
+ self.remove_alert(self._editing_alert)
+ self._editing_alert = None
+
+ def _add_editing_alert(self, title, message, operation, callback, data):
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ ok_icon = Icon(icon_name='dialog-ok')
+
+ alert = Alert()
+ alert.props.title = title
+ alert.props.msg = message
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
+ alert.add_button(gtk.RESPONSE_OK, operation, ok_icon)
+ alert.connect('response', callback, data)
+ alert.show()
+
+ self._remove_editing_alert()
+ self._editing_alert = alert
+ self.add_alert(alert)
+
+ def __edit_erase_activated_cb(self, toolbar):
+ list_model = self._list_view.get_model()
+ entries_set = list_model.get_selection()
+ entries_len = len(entries_set)
+
+ message = ngettext('Do you want to erase %d entry?',
+ 'Do you want to erase %d entries?',
+ entries_len) % entries_len
+
+ self._add_editing_alert(_('Confirm erase'), message, _('Erase'),
+ self.__edit_erase_confirm_cb, entries_set)
+
+ def __edit_erase_confirm_cb(self, alert, response_id, entries_set):
+ self._remove_editing_alert()
+ if response_id == gtk.RESPONSE_OK:
+ gobject.idle_add(self._edit_erase_selection, entries_set)
+
+ def _edit_erase_selection(self, entries_set):
+ mount_path = self._list_view.get_mountpoint()
+ model.delete_entries(entries_set, mount_path)
+
+ def __edit_copy_activated_cb(self, toolbar, mount_info, mount_path):
+ list_model = self._list_view.get_model()
+ entries_set = list_model.get_selection()
+ entries_len = len(entries_set)
+
+ message = ngettext('Do you want to copy %d entry to %s?',
+ 'Do you want to copy %d entries to %s?',
+ entries_len) % (entries_len, mount_info)
+
+ self._add_editing_alert(_('Confirm copy'), message, _('Copy'),
+ self.__edit_copy_confirm_cb, (entries_set, mount_path))
+
+ def __edit_copy_confirm_cb(self, alert, response_id, data):
+ entries_set, mount_path = data
+ self._remove_editing_alert()
+ if response_id == gtk.RESPONSE_OK:
+ gobject.idle_add(self._edit_copy_selection,
+ entries_set, mount_path)
+
+ def _edit_copy_selection(self, entries_set, mount_path):
+ status, message = model.copy_entries(entries_set, mount_path)
+
+ if status is False:
+ alert = ErrorAlert(title=_('Copying error'), msg=message)
+ alert.connect('response', self.__alert_response_cb)
+ alert.show()
+ self.add_alert(alert)
+
+
def _key_press_event_cb(self, widget, event):
keyname = gtk.gdk.keyval_name(event.keyval)
if keyname == 'Escape':
@@ -208,6 +304,15 @@ class JournalActivity(JournalWindow):
def __clear_clicked_cb(self, list_view):
self._main_toolbox.search_toolbar.clear_query()
+ def __select_toggled_cb(self, list_view, mode):
+ logging.debug('Selection mode is %s', str(mode))
+ self._editing_mode = mode
+
+ # HACK: Don't exit detail view
+ if self.toolbar_box != self._detail_toolbox:
+ self.show_main_view()
+
+
def __go_back_clicked_cb(self, detail_view):
self.show_main_view()
@@ -216,15 +321,28 @@ class JournalActivity(JournalWindow):
self.show_main_view()
def show_main_view(self):
- if self.toolbar_box != self._main_toolbox:
- self.set_toolbar_box(self._main_toolbox)
- self._main_toolbox.show()
+ self._remove_editing_alert()
+
+ if self._editing_mode:
+ #HACK: Hide current mount point copy-to option
+ mount_point = self._list_view.get_mountpoint()
+ edit_toolbar = self._edit_toolbox.edit_toolbar
+ edit_toolbar.arrange_copy_options(mount_point)
+
+ toolbox = self._edit_toolbox
+ else:
+ toolbox = self._main_toolbox
+ if self.toolbar_box != toolbox:
+ self.set_toolbar_box(toolbox)
+ toolbox.show()
if self.canvas != self._main_view:
self.set_canvas(self._main_view)
self._main_view.show()
def _show_secondary_view(self, object_id):
+ self._remove_editing_alert()
+
metadata = model.get(object_id)
try:
self._detail_toolbox.entry_toolbar.set_metadata(metadata)
@@ -322,11 +440,15 @@ class JournalActivity(JournalWindow):
logging.debug('window_state_event_cb %r', self)
if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
state = event.new_window_state
- visible = not state & gtk.gdk.WINDOW_STATE_ICONIFIED
+ self._iconified = state & gtk.gdk.WINDOW_STATE_ICONIFIED
+ visible = not self._iconified
self._list_view.set_is_visible(visible)
+
def __visibility_notify_event_cb(self, window, event):
logging.debug('visibility_notify_event_cb %r', self)
+ if self._iconified:
+ return
visible = event.state != gtk.gdk.VISIBILITY_FULLY_OBSCURED
self._list_view.set_is_visible(visible)
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index 2aa4153..4b5786b 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -526,6 +526,182 @@ class EntryToolbar(gtk.Toolbar):
palette.menu.append(menu_item)
menu_item.show()
+class EditToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.edit_toolbar = EditToolbar()
+ self.add_toolbar('', self.edit_toolbar)
+ self.edit_toolbar.show()
+
+
+class EditToolbar(gtk.Toolbar):
+
+ __gsignals__ = {
+ 'edit-none': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'edit-all': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'edit-erase': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'edit-copy': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str, str]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ none_button = ToolButton('select-none')
+ none_button.set_tooltip(_('Select none'))
+ none_button.connect('clicked', self.__none_clicked_cb)
+ none_button.show()
+ self.add(none_button)
+
+ all_button = ToolButton('select-all')
+ all_button.set_tooltip(_('Select all'))
+ all_button.connect('clicked', self.__all_clicked_cb)
+ all_button.show()
+ self.add(all_button)
+
+ separator = gtk.SeparatorToolItem()
+ separator.show()
+ self.add(separator)
+
+ erase_button = ToolButton('edit-delete')
+ erase_button.set_tooltip(_('Erase'))
+ erase_button.connect('clicked', self.__erase_clicked_cb)
+ erase_button.show()
+ self.add(erase_button)
+
+ self._copy_button = EditCopyButton()
+ self._copy_button.connect('button-edit-copy', self.__copy_clicked_cb)
+ self._copy_button.show()
+ self.add(self._copy_button)
+
+ def __none_clicked_cb(self, button):
+ logging.debug('Edit toolbar emitting none signal')
+ self.emit('edit-none')
+
+ def __all_clicked_cb(self, button):
+ logging.debug('Edit toolbar emitting all signal')
+ self.emit('edit-all')
+
+ def __erase_clicked_cb(self, button):
+ logging.debug('Edit toolbar emitting erase signal')
+ self.emit('edit-erase')
+
+ def __copy_clicked_cb(self, button, mount_info, mount_path):
+ logging.debug('Edit toolbar emitting copy signal')
+ self.emit('edit-copy', mount_info, mount_path)
+
+ def arrange_copy_options(self, mount_path):
+ self._copy_button.arrange_options(mount_path)
+
+
+class EditCopyButton(ToolButton):
+ __gtype_name__ = 'JournalEditCopyButton'
+
+ __gsignals__ = {
+ 'button-edit-copy': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str, str]))
+ }
+
+ _MIN_ITEMS = 2
+
+ def __init__(self):
+ ToolButton.__init__(self)
+
+ self.props.tooltip = _('Copy to')
+ self.props.icon_name = 'edit-copy'
+
+ self._empty_item = MenuItem(_('No options available'))
+ self._empty_item.set_sensitive(False)
+ self.props.palette.menu.insert(self._empty_item, -1)
+ self._empty_item.show()
+
+ self._add_menuitem('activity-journal', _('the journal'), '/')
+
+ monitor = gio.volume_monitor_get()
+ for mount in monitor.get_mounts():
+ self._add_menuitem_mount(mount)
+ self._check_availability()
+
+ self._mount_added_hid = monitor.connect('mount-added',
+ self.__mount_added_cb)
+ self._mount_removed_hid = monitor.connect('mount-removed',
+ self.__mount_removed_cb)
+ self.connect('clicked', self.__show_options_palette_cb)
+
+ def __destroy_cb(self, button):
+ monitor = gio.volume_monitor_get()
+ monitor.disconnect(self._mount_added_hid)
+ monitor.disconnect(self._mount_removed_hid)
+
+ def __mount_added_cb(self, monitor, mount):
+ self._add_menuitem_mount(mount)
+ self._check_availability()
+
+ def __mount_removed_cb(self, monitor, mount):
+ mount_path = mount.get_root().get_path()
+ menu = self.props.palette.menu
+ for item in menu.get_children():
+ if not isinstance(item, EditCopyItem):
+ continue
+ if mount_path == item.mount_path:
+ menu.remove(item)
+ self._check_availability()
+
+ def __show_options_palette_cb(self, button):
+ self.props.palette.popup(immediate=True, state=1)
+
+ def __copy_activated_cb(self, item):
+ self.emit('button-edit-copy', item.mount_info, item.mount_path)
+
+ def _add_menuitem(self, icon_name, label, mount):
+ item = EditCopyItem(icon_name=icon_name,
+ text_label=label,
+ mount_path=mount)
+ item.connect('activate', self.__copy_activated_cb)
+ item.show()
+ self.props.palette.menu.insert(item, -1)
+
+ def _add_menuitem_mount(self, mount):
+ icon_theme = gtk.icon_theme_get_default()
+ for name in mount.get_icon().props.names:
+ if icon_theme.has_icon(name):
+ icon_name=name
+ break
+ self._add_menuitem(icon_name,
+ mount.get_name(),
+ mount.get_root().get_path())
+
+ def _check_availability(self):
+ menu_items = self.props.palette.menu.get_children()
+ if len(menu_items) > self._MIN_ITEMS:
+ self._empty_item.hide()
+ else:
+ self._empty_item.show()
+
+ def arrange_options(self, mount_path):
+ menu_items = self.props.palette.menu.get_children()
+ for item in menu_items:
+ if not isinstance(item, EditCopyItem):
+ continue
+ if mount_path == item.mount_path:
+ item.hide()
+ else:
+ item.show()
+
+
+class EditCopyItem(MenuItem):
+ __gtype_name__ = 'JournalEditCopyItem'
+
+ def __init__(self, icon_name, text_label, mount_path):
+ MenuItem.__init__(self, icon_name=icon_name, text_label=text_label)
+ self.mount_path = mount_path
+ self.mount_info = text_label
+
class SortingButton(ToolButton):
__gtype_name__ = 'JournalSortingButton'
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
index 417ff61..833c823 100644
--- a/src/jarabe/journal/listmodel.py
+++ b/src/jarabe/journal/listmodel.py
@@ -40,6 +40,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
__gsignals__ = {
'ready': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'select': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([bool, bool]))
}
COLUMN_UID = 0
@@ -54,6 +55,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
COLUMN_BUDDY_1 = 9
COLUMN_BUDDY_2 = 10
COLUMN_BUDDY_3 = 11
+ COLUMN_SELECT = 12
_COLUMN_TYPES = {
COLUMN_UID: str,
@@ -68,9 +70,10 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
COLUMN_BUDDY_1: object,
COLUMN_BUDDY_3: object,
COLUMN_BUDDY_2: object,
+ COLUMN_SELECT: bool
}
- _PAGE_SIZE = 10
+ _PAGE_SIZE = 500
def __init__(self, query):
gobject.GObject.__init__(self)
@@ -80,6 +83,10 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
self._result_set = model.find(query, ListModel._PAGE_SIZE)
self._temp_drag_file_path = None
+ # Multi-selection stuff
+ self._selection = set()
+ self._query_set_cache = set()
+
# HACK: The view will tell us that it is resizing so the model can
# avoid hitting D-Bus and disk.
self.view_is_resizing = False
@@ -119,6 +126,11 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
return None
if index == self._last_requested_index:
+ # HACK: avoid redrawing the whole view just for one row
+ selected = (self._cached_row[ListModel.COLUMN_UID] \
+ in self._selection)
+ self._cached_row[ListModel.COLUMN_SELECT] = selected
+
return self._cached_row[column]
if index >= self._result_set.length:
@@ -197,6 +209,9 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
continue
self._cached_row.append(None)
+ selected = (metadata['uid'] in self._selection)
+ self._cached_row.append(selected)
+
return self._cached_row[column]
@@ -241,3 +256,45 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource):
return True
return False
+
+ def _get_query_set(self):
+ if self._query_set_cache:
+ return self._query_set_cache
+ query_set = set()
+ def collect(model, path, iter):
+ query_set.add(model[path][ListModel.COLUMN_UID])
+ self.foreach(collect)
+ self._query_set_cache = query_set
+ return query_set
+
+ def get_selection(self):
+ return self._selection.copy()
+
+ def add_selection(self, selection):
+ if type(selection) is set:
+ query_set = self._get_query_set()
+ selection = selection.intersection(query_set)
+ self._selection = self._selection.union(selection)
+ self._emit_select()
+
+ def set_selection_all(self):
+ query_set = self._get_query_set()
+ self._selection = self._selection.union(query_set)
+ self._emit_select(refresh_view=True)
+
+ def set_selection_none(self):
+ self._selection = set()
+ self._emit_select(refresh_view=True)
+
+ def toggle_selection(self, path):
+ uid = self[path][ListModel.COLUMN_UID]
+ if uid in self._selection:
+ self._selection.discard(uid)
+ else:
+ self._selection.add(uid)
+ self._emit_select()
+
+ def _emit_select(self, refresh_view=False):
+ status = not (not self._selection)
+ self.emit('select', status, refresh_view)
+
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index 57836f2..ce71f54 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -64,6 +64,10 @@ class BaseListView(gtk.Bin):
__gsignals__ = {
'clear-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'clear-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'select-toggled': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([bool]))
}
def __init__(self):
@@ -115,6 +119,12 @@ class BaseListView(gtk.Bin):
model.updated.connect(self.__model_updated_cb)
model.deleted.connect(self.__model_deleted_cb)
+ # Multi-selection stuff
+ self._selection_cache = set()
+
+ def get_mountpoint(self):
+ return self._query.get('mountpoints', [''])[0]
+
def __model_created_cb(self, sender, signal, object_id):
if self._is_new_item_visible(object_id):
self._set_dirty()
@@ -134,10 +144,31 @@ class BaseListView(gtk.Bin):
else:
return object_id.startswith(self._query['mountpoints'][0])
+ def __selected_cb(self, cell, path):
+ self._model.toggle_selection(path)
+
+ def get_model(self):
+ return self._model
+
def _add_columns(self):
+
+ cell_select = gtk.CellRendererToggle()
+ cell_select.props.indicator_size = style.zoom(26)
+ cell_select.props.activatable = True
+ cell_select.connect('toggled', self.__selected_cb)
+
+ column = gtk.TreeViewColumn()
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+ column.props.fixed_width = style.GRID_CELL_SIZE
+ column.pack_start(cell_select)
+ column.add_attribute(cell_select, "active", ListModel.COLUMN_SELECT)
+ self.tree_view.append_column(column)
+
+
cell_favorite = CellRendererFavorite(self.tree_view)
cell_favorite.connect('clicked', self.__favorite_clicked_cb)
+
column = gtk.TreeViewColumn()
column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
column.props.fixed_width = cell_favorite.props.width
@@ -283,14 +314,34 @@ class BaseListView(gtk.Bin):
if self._model is not None:
self._model.stop()
+ self._manage_selection_cache()
self._dirty = False
self._model = ListModel(self._query)
+ self._model.connect('select', self.__model_select_cb)
self._model.connect('ready', self.__model_ready_cb)
self._model.connect('progress', self.__model_progress_cb)
self._model.setup()
+ def _manage_selection_cache(self):
+ # Discard from cache elements that might not be selected anymore
+ self._selection_cache = \
+ self._selection_cache.difference(self._model._query_set_cache)
+ # Add to cache elements that are selected
+ self._selection_cache = \
+ self._selection_cache.union(self._model.get_selection())
+
+ def __model_select_cb(self, tree_model, status, refresh_view):
+ if refresh_view:
+ self._refresh_view(tree_model)
+ self.emit('select-toggled', status)
+
+
def __model_ready_cb(self, tree_model):
+ self._model.add_selection(self._selection_cache)
+ self._refresh_view(tree_model)
+
+ def _refresh_view(self, tree_model):
self._stop_progress_bar()
self._scroll_position = self.tree_view.props.vadjustment.props.value
@@ -413,6 +464,8 @@ class BaseListView(gtk.Bin):
self.add(self._scrolled_window)
self._scrolled_window.show()
+
+
def update_dates(self):
if not self.tree_view.flags() & gtk.REALIZED:
return
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 5285a7c..6897eeb 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -57,6 +57,16 @@ created = dispatch.Signal()
updated = dispatch.Signal()
deleted = dispatch.Signal()
+_sync_signals_enabled = True
+def _emit_created(object_id):
+ global _sync_signals_enabled
+ if _sync_signals_enabled:
+ created.send(None, object_id=object_id)
+
+def _emit_deleted(object_id):
+ global _sync_signals_enabled
+ if _sync_signals_enabled:
+ deleted.send(None, object_id=object_id)
class _Cache(object):
@@ -420,6 +430,43 @@ class InplaceResultSet(BaseResultSet):
self._pending_files.append(dir_path + '/' + entry)
return
+def _set_signals_state(state, callback=None, data=None):
+ global _sync_signals_enabled
+ _sync_signals_enabled = state
+ if callback:
+ callback(data)
+
+def copy_entries(entries_set, mount_point):
+ _set_signals_state(False)
+ status, message = True, ''
+ for entry_uid in entries_set:
+ try:
+ metadata = get(entry_uid)
+ copy(metadata, mount_point)
+ except ValueError:
+ logging.warning('Entry %s has nothing to copied', entry_uid)
+ except (OSError, IOError):
+ status, message = False, _('No available space to continue')
+ break
+ gobject.idle_add(_set_signals_state, True)
+ return (status, message)
+
+def delete_entries(entries_set, mount_point):
+ _set_signals_state(False)
+ for entry_uid in entries_set:
+ try:
+ delete(entry_uid)
+ except (OSError, IOError):
+ logging.warning('Entry %s could not be deleted', entry_uid)
+ gobject.idle_add(_set_signals_state, True,
+ __post_delete_entries_cb, mount_point)
+
+def __post_delete_entries_cb(mount_point):
+ if mount_point is '/':
+ mount_point = 'abcde'
+ _emit_deleted(mount_point)
+
+
def _get_file_metadata(path, stat, fetch_preview=True):
"""Return the metadata from the corresponding file.
@@ -504,16 +551,14 @@ def _get_datastore():
def _datastore_created_cb(object_id):
- created.send(None, object_id=object_id)
-
+ _emit_created(object_id)
def _datastore_updated_cb(object_id):
updated.send(None, object_id=object_id)
def _datastore_deleted_cb(object_id):
- deleted.send(None, object_id=object_id)
-
+ _emit_deleted(object_id)
def find(query_, page_size):
"""Returns a ResultSet
@@ -611,7 +656,7 @@ def delete(object_id):
except EnvironmentError:
logging.error('Could not remove metadata=%s '
'for file=%s', old_file, filename)
- deleted.send(None, object_id=object_id)
+ _emit_deleted(object_id)
def copy(metadata, mount_point):
@@ -746,7 +791,7 @@ def _write_entry_on_external_device(metadata, file_path):
metadata_dir_path)
object_id = destination_path
- created.send(None, object_id=object_id)
+ _emit_created(object_id)
return object_id