Beispiel #1
0
    def refresh(self):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()

        if self._model is not None:
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
Beispiel #2
0
    def _do_refresh(self, new_query=False):
        if self._model is not None:
            if new_query:
                self._backup_selected = None
            else:
                self._backup_selected = self._model.get_selected_items()
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
        window = self.get_toplevel().get_window()
        if window is not None:
            window.set_cursor(None)
Beispiel #3
0
    def refresh(self, new_query=False):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()

        if self._model is not None:
            if new_query:
                self._backup_selected = None
            else:
                self._backup_selected = self._model.get_selected_items()
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
Beispiel #4
0
    def refresh(self):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()

        if self._model is not None:
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
Beispiel #5
0
    def refresh(self, new_query=False):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()

        if self._model is not None:
            if new_query:
                self._backup_selected = None
            else:
                self._backup_selected = self._model.get_selected_items()
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
Beispiel #6
0
    def _do_refresh(self, new_query=False):
        if self._model is not None:
            if new_query:
                self._backup_selected = None
            else:
                self._backup_selected = self._model.get_selected_items()
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
        window = self.get_toplevel().get_window()
        if window is not None:
            window.set_cursor(None)
Beispiel #7
0
class BaseListView(Gtk.Bin):
    __gtype_name__ = 'JournalBaseListView'

    __gsignals__ = {
        'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])),
        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ([int])),
        'detail-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([object])),
        'volume-error': (GObject.SignalFlags.RUN_FIRST, None, ([str, str])),
    }

    def __init__(self, journalactivity, enable_multi_operations=False):
        self._query = {}
        self._journalactivity = journalactivity
        self._enable_multi_operations = enable_multi_operations
        self._model = None
        self._progress_bar = None
        self._last_progress_bar_pulse = None
        self._scroll_position = 0.

        Gtk.Bin.__init__(self)

        self.connect('map', self.__map_cb)
        self.connect('unmap', self.__unmap_cb)
        self.connect('destroy', self.__destroy_cb)

        self._scrolled_window = Gtk.ScrolledWindow()
        self._scrolled_window.set_policy(Gtk.PolicyType.NEVER,
                                         Gtk.PolicyType.AUTOMATIC)
        self.add(self._scrolled_window)
        self._scrolled_window.show()

        self.tree_view = TreeView(self._journalactivity)
        self.tree_view.connect('detail-clicked', self.__detail_clicked_cb)
        self.tree_view.connect('volume-error', self.__volume_error_cb)
        selection = self.tree_view.get_selection()
        selection.set_mode(Gtk.SelectionMode.NONE)
        self.tree_view.props.fixed_height_mode = True
        self._scrolled_window.add(self.tree_view)
        self.tree_view.show()

        self.cell_title = None
        self.cell_icon = None
        self._title_column = None
        self.sort_column = None
        self._add_columns()
        scrolling_detector = ScrollingDetector(self._scrolled_window)
        self.tree_view.connect_to_scroller(scrolling_detector)

        self.enable_drag_and_copy()

        # Auto-update stuff
        self._fully_obscured = True
        self._updates_disabled = False
        self._dirty = False
        self._refresh_idle_handler = None
        self._update_dates_timer = None
        self._backup_selected = None

        model.created.connect(self.__model_created_cb)
        model.updated.connect(self.__model_updated_cb)
        model.deleted.connect(self.__model_deleted_cb)

    def enable_drag_and_copy(self):
        self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
                                                [('text/uri-list', 0, 0),
                                                 ('journal-object-id', 0, 0)],
                                                Gdk.DragAction.COPY)

    def disable_drag_and_copy(self):
        self.tree_view.unset_rows_drag_source()

    def __model_created_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_updated_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_deleted_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def _is_new_item_visible(self, object_id):
        """Check if the created item is part of the currently selected view"""
        if self._query['mountpoints'] == ['/']:
            return not object_id.startswith('/')
        else:
            return object_id.startswith(self._query['mountpoints'][0])

    def _add_columns(self):
        if self._enable_multi_operations:
            cell_select = Gtk.CellRendererToggle()
            cell_select.connect('toggled', self.__cell_select_toggled_cb)
            cell_select.props.activatable = True
            cell_select.props.xpad = style.DEFAULT_PADDING
            cell_select.props.indicator_size = style.zoom(26)

            column = Gtk.TreeViewColumn()
            column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
            column.props.fixed_width = style.GRID_CELL_SIZE
            column.pack_start(cell_select, True)
            column.set_cell_data_func(cell_select, self.__select_set_data_cb)
            self.tree_view.append_column(column)

        cell_favorite = CellRendererFavorite()
        cell_favorite.connect('clicked', self._favorite_clicked_cb)

        column = Gtk.TreeViewColumn()
        column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        column.props.fixed_width = cell_favorite.props.width
        column.pack_start(cell_favorite, True)
        column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb)
        self.tree_view.append_column(column)

        self.cell_icon = CellRendererActivityIcon()

        column = Gtk.TreeViewColumn()
        self.tree_view.icon_activity_column = column
        column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        column.props.fixed_width = self.cell_icon.props.width
        column.pack_start(self.cell_icon, True)
        column.add_attribute(self.cell_icon, 'file-name',
                             ListModel.COLUMN_ICON)
        column.add_attribute(self.cell_icon, 'xo-color',
                             ListModel.COLUMN_ICON_COLOR)
        self.tree_view.append_column(column)
        self.icon_activity_column = column

        self.cell_title = Gtk.CellRendererText()
        self.cell_title.props.ellipsize = style.ELLIPSIZE_MODE_DEFAULT
        self.cell_title.props.ellipsize_set = True

        self._title_column = Gtk.TreeViewColumn()
        self._title_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self._title_column.props.expand = True
        self._title_column.props.clickable = True
        self._title_column.pack_start(self.cell_title, True)
        self._title_column.add_attribute(self.cell_title, 'markup',
                                         ListModel.COLUMN_TITLE)
        self.tree_view.append_column(self._title_column)

        for column_index in [
                ListModel.COLUMN_BUDDY_1, ListModel.COLUMN_BUDDY_2,
                ListModel.COLUMN_BUDDY_3
        ]:

            buddies_column = Gtk.TreeViewColumn()
            buddies_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
            self.tree_view.append_column(buddies_column)

            cell_icon = CellRendererBuddy(column_index=column_index)
            buddies_column.pack_start(cell_icon, True)
            buddies_column.props.fixed_width += cell_icon.props.width
            buddies_column.add_attribute(cell_icon, 'buddy', column_index)
            buddies_column.set_cell_data_func(cell_icon,
                                              self.__buddies_set_data_cb)
            self.tree_view.buddies_columns.append(buddies_column)

        cell_progress = Gtk.CellRendererProgress()
        cell_progress.props.ypad = style.GRID_CELL_SIZE / 4
        buddies_column.pack_start(cell_progress, True)
        buddies_column.add_attribute(cell_progress, 'value',
                                     ListModel.COLUMN_PROGRESS)
        buddies_column.set_cell_data_func(cell_progress,
                                          self.__progress_data_cb)

        cell_text = Gtk.CellRendererText()
        cell_text.props.xalign = 1

        # Measure the required width for a date in the form of "10 hours, 10
        # minutes ago"
        timestamp = time.time() - 10 * 60 - 10 * 60 * 60
        date = util.timestamp_to_elapsed_string(timestamp)
        date_width = self._get_width_for_string(date)

        self.sort_column = Gtk.TreeViewColumn()
        self.sort_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self.sort_column.props.fixed_width = date_width
        self.sort_column.set_alignment(1)
        self.sort_column.props.resizable = True
        self.sort_column.props.clickable = True
        self.sort_column.pack_start(cell_text, True)
        self.sort_column.add_attribute(cell_text, 'text',
                                       ListModel.COLUMN_TIMESTAMP)
        self.tree_view.append_column(self.sort_column)

    def _get_width_for_string(self, text):
        # Add some extra margin
        text = text + 'aaaaa'

        widget = Gtk.Label(label='')
        context = widget.get_pango_context()
        layout = Pango.Layout(context)
        layout.set_text(text, len(text))
        width, height_ = layout.get_pixel_size()
        return width

    def do_size_allocate(self, allocation):
        self.set_allocation(allocation)
        self.get_child().size_allocate(allocation)

    def do_size_request(self, requisition):
        requisition.width, requisition.height = \
            self.get_child().size_request()

    def __destroy_cb(self, widget):
        if self._model is not None:
            self._model.stop()

    def __buddies_set_data_cb(self, column, cell, tree_model, tree_iter, data):
        buddy = tree_model.do_get_value(tree_iter, cell._model_column_index)
        if buddy is None:
            cell.props.visible = False
            return
        logging.error('__buddies_set_data_cb %s', buddy)
        # FIXME workaround for pygobject bug, see
        # https://bugzilla.gnome.org/show_bug.cgi?id=689277
        #
        # add_attribute with 'buddy' attribute in the cell should take
        # care of setting it.
        cell.props.buddy = buddy

        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress >= 100

    def __progress_data_cb(self, column, cell, tree_model, tree_iter, data):
        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress < 100

    def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter,
                               data):
        favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
        if favorite:
            cell.props.xo_color = profile.get_color()
        else:
            cell.props.xo_color = None

    def _favorite_clicked_cb(self, cell, path):
        row = self._model[path]
        metadata = model.get(row[ListModel.COLUMN_UID])
        if not model.is_editable(metadata):
            return
        if metadata.get('keep', 0) == '1':
            metadata['keep'] = '0'
        else:
            metadata['keep'] = '1'
        model.write(metadata, update_mtime=False)

    def __select_set_data_cb(self, column, cell, tree_model, tree_iter, data):
        uid = tree_model[tree_iter][ListModel.COLUMN_UID]
        if uid is None:
            return
        cell.props.active = self._model.is_selected(uid)

    def __cell_select_toggled_cb(self, cell, path):
        tree_iter = self._model.get_iter(path)
        uid = self._model[tree_iter][ListModel.COLUMN_UID]
        self._model.set_selected(uid, not cell.get_active())
        self.emit('selection-changed', len(self._model.get_selected_items()))

    def update_with_query(self, query_dict):
        logging.debug('ListView.update_with_query')
        if 'order_by' not in query_dict:
            query_dict['order_by'] = ['+timestamp']
        if query_dict['order_by'] != self._query.get('order_by'):
            property_ = query_dict['order_by'][0][1:]
            cell_text = self.sort_column.get_cells()[0]
            self.sort_column.set_attributes(
                cell_text,
                text=getattr(ListModel, 'COLUMN_' + property_.upper(),
                             ListModel.COLUMN_TIMESTAMP))
        self._query = query_dict
        self.refresh(new_query=True)

    def refresh(self, new_query=False):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()
        window = self.get_toplevel().get_window()
        if window is not None:
            window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
        GObject.idle_add(self._do_refresh, new_query)

    def _do_refresh(self, new_query=False):
        if self._model is not None:
            if new_query:
                self._backup_selected = None
            else:
                self._backup_selected = self._model.get_selected_items()
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()
        window = self.get_toplevel().get_window()
        if window is not None:
            window.set_cursor(None)

    def __model_ready_cb(self, tree_model):
        self._stop_progress_bar()

        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug('ListView.__model_ready_cb %r', self._scroll_position)

        x11_window = self.tree_view.get_window()

        if x11_window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().hide()

        # if the selection was preserved, restore it
        if self._backup_selected is not None:
            tree_model.restore_selection(self._backup_selected)
            self.emit('selection-changed', len(self._backup_selected))

        # Cannot set it up earlier because will try to access the model
        # and it needs to be ready.
        self.tree_view.set_model(self._model)

        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()

        if x11_window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().show()

        if len(tree_model) == 0:
            documents_path = model.get_documents_path()
            if self._is_query_empty():
                if self._query['mountpoints'] == ['/']:
                    self._show_message(_('Your Journal is empty'))
                elif documents_path and self._query['mountpoints'] == \
                        [documents_path]:
                    self._show_message(_('Your documents folder is empty'))
                else:
                    self._show_message(_('The device is empty'))
            else:
                self._show_message(_('No matching entries'),
                                   show_clear_query=self._can_clear_query())
        else:
            self._clear_message()

    def _can_clear_query(self):
        return True

    def __map_cb(self, widget):
        logging.debug('ListView.__map_cb %r', self._scroll_position)
        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()
        self.set_is_visible(True)

    def __unmap_cb(self, widget):
        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug('ListView.__unmap_cb %r', self._scroll_position)
        self.set_is_visible(False)

    def _is_query_empty(self):
        # FIXME: This is a hack, we shouldn't have to update this every time
        # a new search term is added.
        return not (self._query.get('query') or self._query.get('mime_type')
                    or self._query.get('keep') or self._query.get('mtime')
                    or self._query.get('activity'))

    def __model_progress_cb(self, tree_model):
        if self._progress_bar is None:
            self._start_progress_bar()

        if time.time() - self._last_progress_bar_pulse > 0.05:
            self._progress_bar.pulse()
            self._last_progress_bar_pulse = time.time()

    def _start_progress_bar(self):
        alignment = Gtk.Alignment.new(xalign=0.5,
                                      yalign=0.5,
                                      xscale=0.5,
                                      yscale=0)
        self.remove(self.get_child())
        self.add(alignment)
        alignment.show()

        self._progress_bar = Gtk.ProgressBar()
        self._progress_bar.props.pulse_step = 0.01
        self._last_progress_bar_pulse = time.time()
        alignment.add(self._progress_bar)
        self._progress_bar.show()

    def _stop_progress_bar(self):
        if self._progress_bar is None:
            return
        self.remove(self.get_child())
        self.add(self._scrolled_window)
        self._progress_bar = None

    def _show_message(self, message, show_clear_query=False):
        self.remove(self.get_child())

        background_box = Gtk.EventBox()
        background_box.modify_bg(Gtk.StateType.NORMAL,
                                 style.COLOR_WHITE.get_gdk_color())
        self.add(background_box)

        alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1)
        background_box.add(alignment)

        box = Gtk.VBox()
        alignment.add(box)

        icon = Icon(pixel_size=style.LARGE_ICON_SIZE,
                    icon_name='activity-journal',
                    stroke_color=style.COLOR_BUTTON_GREY.get_svg(),
                    fill_color=style.COLOR_TRANSPARENT.get_svg())
        box.pack_start(icon, expand=True, fill=False, padding=0)

        label = Gtk.Label()
        color = style.COLOR_BUTTON_GREY.get_html()
        label.set_markup('<span weight="bold" color="%s">%s</span>' %
                         (color, GLib.markup_escape_text(message)))
        box.pack_start(label, expand=True, fill=False, padding=0)

        if show_clear_query:
            button_box = Gtk.HButtonBox()
            button_box.set_layout(Gtk.ButtonBoxStyle.CENTER)
            box.pack_start(button_box, False, True, 0)
            button_box.show()

            button = Gtk.Button(label=_('Clear search'))
            button.connect('clicked', self.__clear_button_clicked_cb)
            button.props.image = Icon(icon_name='dialog-cancel',
                                      pixel_size=style.SMALL_ICON_SIZE)
            button_box.pack_start(button, expand=True, fill=False, padding=0)

        background_box.show_all()

    def __clear_button_clicked_cb(self, button):
        self.emit('clear-clicked')

    def _clear_message(self):
        if self.get_child() == self._scrolled_window:
            return
        self.remove(self.get_child())
        self.add(self._scrolled_window)
        self._scrolled_window.show()

    def update_dates(self):
        if not self.tree_view.get_realized():
            return
        visible_range = self.tree_view.get_visible_range()
        if visible_range is None:
            return

        logging.debug('ListView.update_dates')

        path, end_path = visible_range
        tree_model = self.tree_view.get_model()

        while True:
            cel_rect = self.tree_view.get_cell_area(path, self.sort_column)
            x, y = self.tree_view.convert_tree_to_widget_coords(
                cel_rect.x, cel_rect.y)
            self.tree_view.queue_draw_area(x, y, cel_rect.width,
                                           cel_rect.height)
            if path == end_path:
                break
            else:
                next_iter = tree_model.iter_next(tree_model.get_iter(path))
                path = tree_model.get_path(next_iter)

    def _set_dirty(self):
        if self._fully_obscured or self._updates_disabled:
            self._dirty = True
        else:
            self.refresh()

    def disable_updates(self):
        self._updates_disabled = True

    def enable_updates(self):
        self._updates_disabled = False
        if self._dirty:
            self.refresh()

    def set_is_visible(self, visible):
        if visible != self._fully_obscured:
            return

        logging.debug('canvas_visibility_notify_event_cb %r', visible)
        if visible:
            self._fully_obscured = False
            if self._dirty:
                self.refresh()
            if self._update_dates_timer is None:
                logging.debug('Adding date updating timer')
                self._update_dates_timer = \
                    GObject.timeout_add_seconds(UPDATE_INTERVAL,
                                                self.__update_dates_timer_cb)
        else:
            self._fully_obscured = True
            if self._update_dates_timer is not None:
                logging.debug('Remove date updating timer')
                GObject.source_remove(self._update_dates_timer)
                self._update_dates_timer = None

    def __update_dates_timer_cb(self):
        self.update_dates()
        return True

    def get_model(self):
        return self._model

    def select_all(self):
        self.get_model().select_all()
        self.tree_view.queue_draw()
        self.emit('selection-changed', len(self._model.get_selected_items()))

    def select_none(self):
        self.get_model().select_none()
        self.tree_view.queue_draw()
        self.emit('selection-changed', len(self._model.get_selected_items()))

    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)
Beispiel #8
0
class BaseListView(Gtk.Bin):
    __gtype_name__ = 'JournalBaseListView'

    __gsignals__ = {
        'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])),
    }

    def __init__(self):
        self._query = {}
        self._model = None
        self._progress_bar = None
        self._last_progress_bar_pulse = None
        self._scroll_position = 0.

        Gtk.Bin.__init__(self)

        self.connect('map', self.__map_cb)
        self.connect('unrealize', self.__unrealize_cb)
        self.connect('destroy', self.__destroy_cb)

        self._scrolled_window = Gtk.ScrolledWindow()
        self._scrolled_window.set_policy(Gtk.PolicyType.NEVER,
                                         Gtk.PolicyType.AUTOMATIC)
        self.add(self._scrolled_window)
        self._scrolled_window.show()

        self.tree_view = TreeView()
        selection = self.tree_view.get_selection()
        selection.set_mode(Gtk.SelectionMode.NONE)
        self.tree_view.props.fixed_height_mode = True
        self._scrolled_window.add(self.tree_view)
        self.tree_view.show()

        self.cell_title = None
        self.cell_icon = None
        self._title_column = None
        self.sort_column = None
        self._add_columns()

        self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
                                                [('text/uri-list', 0, 0),
                                                 ('journal-object-id', 0, 0)],
                                                Gdk.DragAction.COPY)

        # Auto-update stuff
        self._fully_obscured = True
        self._dirty = False
        self._refresh_idle_handler = None
        self._update_dates_timer = None

        model.created.connect(self.__model_created_cb)
        model.updated.connect(self.__model_updated_cb)
        model.deleted.connect(self.__model_deleted_cb)

    def __model_created_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_updated_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_deleted_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def _is_new_item_visible(self, object_id):
        """Check if the created item is part of the currently selected view"""
        if self._query['mountpoints'] == ['/']:
            return not object_id.startswith('/')
        else:
            return object_id.startswith(self._query['mountpoints'][0])

    def _add_columns(self):
        cell_favorite = CellRendererFavorite(self.tree_view)
        cell_favorite.connect('clicked', self.__favorite_clicked_cb)

        column = Gtk.TreeViewColumn()
        column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        column.props.fixed_width = cell_favorite.props.width
        column.pack_start(cell_favorite, True)
        column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb)
        self.tree_view.append_column(column)

        self.cell_icon = CellRendererActivityIcon(self.tree_view)

        column = Gtk.TreeViewColumn()
        column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        column.props.fixed_width = self.cell_icon.props.width
        column.pack_start(self.cell_icon, True)
        column.add_attribute(self.cell_icon, 'file-name',
                             ListModel.COLUMN_ICON)
        column.add_attribute(self.cell_icon, 'xo-color',
                             ListModel.COLUMN_ICON_COLOR)
        self.tree_view.append_column(column)

        self.cell_title = Gtk.CellRendererText()
        self.cell_title.props.ellipsize = Pango.EllipsizeMode.MIDDLE
        self.cell_title.props.ellipsize_set = True

        self._title_column = Gtk.TreeViewColumn()
        self._title_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self._title_column.props.expand = True
        self._title_column.props.clickable = True
        self._title_column.pack_start(self.cell_title, True)
        self._title_column.add_attribute(self.cell_title, 'markup',
                                         ListModel.COLUMN_TITLE)
        self.tree_view.append_column(self._title_column)

        buddies_column = Gtk.TreeViewColumn()
        buddies_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self.tree_view.append_column(buddies_column)

        for column_index in [ListModel.COLUMN_BUDDY_1,
                             ListModel.COLUMN_BUDDY_2,
                             ListModel.COLUMN_BUDDY_3]:
            cell_icon = CellRendererBuddy(self.tree_view,
                                          column_index=column_index)
            buddies_column.pack_start(cell_icon, True)
            buddies_column.props.fixed_width += cell_icon.props.width
            buddies_column.add_attribute(cell_icon, 'buddy', column_index)
            buddies_column.set_cell_data_func(cell_icon,
                                              self.__buddies_set_data_cb)

        cell_progress = Gtk.CellRendererProgress()
        cell_progress.props.ypad = style.GRID_CELL_SIZE / 4
        buddies_column.pack_start(cell_progress, True)
        buddies_column.add_attribute(cell_progress, 'value',
                ListModel.COLUMN_PROGRESS)
        buddies_column.set_cell_data_func(cell_progress,
                                          self.__progress_data_cb)

        cell_text = Gtk.CellRendererText()
        cell_text.props.xalign = 1

        # Measure the required width for a date in the form of "10 hours, 10
        # minutes ago"
        timestamp = time.time() - 10 * 60 - 10 * 60 * 60
        date = util.timestamp_to_elapsed_string(timestamp)
        date_width = self._get_width_for_string(date)

        self.sort_column = Gtk.TreeViewColumn()
        self.sort_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self.sort_column.props.fixed_width = date_width
        self.sort_column.set_alignment(1)
        self.sort_column.props.resizable = True
        self.sort_column.props.clickable = True
        self.sort_column.pack_start(cell_text, True)
        self.sort_column.add_attribute(cell_text, 'text',
                                       ListModel.COLUMN_TIMESTAMP)
        self.tree_view.append_column(self.sort_column)

    def _get_width_for_string(self, text):
        # Add some extra margin
        text = text + 'aaaaa'

        widget = Gtk.Label(label='')
        context = widget.get_pango_context()
        layout = Pango.Layout(context)
        layout.set_text(text, len(text))
        width, height_ = layout.get_pixel_size()
        return width

    def do_size_allocate(self, allocation):
        self.set_allocation(allocation)
        self.get_child().size_allocate(allocation)

    def do_size_request(self, requisition):
        requisition.width, requisition.height = self.get_child().size_request()

    def __destroy_cb(self, widget):
        if self._model is not None:
            self._model.stop()

    def __buddies_set_data_cb(self, column, cell, tree_model,
                              tree_iter, data):
        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress >= 100

    def __progress_data_cb(self, column, cell, tree_model,
                           tree_iter, data):
        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress < 100

    def __favorite_set_data_cb(self, column, cell, tree_model,
                               tree_iter, data):
        favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
        if favorite:
            client = GConf.Client.get_default()
            color = XoColor(client.get_string('/desktop/sugar/user/color'))
            cell.props.xo_color = color
        else:
            cell.props.xo_color = None

    def __favorite_clicked_cb(self, cell, path):
        row = self._model[path]
        metadata = model.get(row[ListModel.COLUMN_UID])
        if not model.is_editable(metadata):
            return
        if metadata.get('keep', 0) == '1':
            metadata['keep'] = '0'
        else:
            metadata['keep'] = '1'
        model.write(metadata, update_mtime=False)

    def update_with_query(self, query_dict):
        logging.debug('ListView.update_with_query')
        if 'order_by' not in query_dict:
            query_dict['order_by'] = ['+timestamp']
        if query_dict['order_by'] != self._query.get('order_by'):
            property_ = query_dict['order_by'][0][1:]
            cell_text = self.sort_column.get_cells()[0]
            self.sort_column.set_attributes(cell_text,
                text=getattr(ListModel, 'COLUMN_' + property_.upper(),
                             ListModel.COLUMN_TIMESTAMP))
        self._query = query_dict

        self.refresh()

    def refresh(self):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()

        if self._model is not None:
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()

    def __model_ready_cb(self, tree_model):
        self._stop_progress_bar()

        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug('ListView.__model_ready_cb %r', self._scroll_position)

        x11_window = self.tree_view.get_window()

        if x11_window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().hide()

        # Cannot set it up earlier because will try to access the model
        # and it needs to be ready.
        self.tree_view.set_model(self._model)

        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()

        if x11_window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().show()

        if len(tree_model) == 0:
            if self._is_query_empty():
                if self._query['mountpoints'] == ['/']:
                    self._show_message(_('Your Journal is empty'))
                elif self._query['mountpoints'] == \
                        [model.get_documents_path()]:
                    self._show_message(_('Your documents folder is empty'))
                else:
                    self._show_message(_('The device is empty'))
            else:
                self._show_message(_('No matching entries'),
                                   show_clear_query=True)
        else:
            self._clear_message()

    def __map_cb(self, widget):
        logging.debug('ListView.__map_cb %r', self._scroll_position)
        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()

    def __unrealize_cb(self, widget):
        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug('ListView.__map_cb %r', self._scroll_position)

    def _is_query_empty(self):
        # FIXME: This is a hack, we shouldn't have to update this every time
        # a new search term is added.
        return not (self._query.get('query') or self._query.get('mime_type') or
                    self._query.get('keep') or self._query.get('mtime') or
                    self._query.get('activity'))

    def __model_progress_cb(self, tree_model):
        if self._progress_bar is None:
            self._start_progress_bar()

        if time.time() - self._last_progress_bar_pulse > 0.05:
            self._progress_bar.pulse()
            self._last_progress_bar_pulse = time.time()

    def _start_progress_bar(self):
        alignment = Gtk.Alignment.new(xalign=0.5, yalign=0.5,
                                      xscale=0.5, yscale=0)
        self.remove(self.get_child())
        self.add(alignment)
        alignment.show()

        self._progress_bar = Gtk.ProgressBar()
        self._progress_bar.props.pulse_step = 0.01
        self._last_progress_bar_pulse = time.time()
        alignment.add(self._progress_bar)
        self._progress_bar.show()

    def _stop_progress_bar(self):
        if self._progress_bar is None:
            return
        self.remove(self.get_child())
        self.add(self._scrolled_window)
        self._progress_bar = None

    def _show_message(self, message, show_clear_query=False):
        self.remove(self.get_child())

        background_box = Gtk.EventBox()
        background_box.modify_bg(Gtk.StateType.NORMAL,
                                 style.COLOR_WHITE.get_gdk_color())
        self.add(background_box)

        alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1)
        background_box.add(alignment)

        box = Gtk.VBox()
        alignment.add(box)

        icon = Icon(pixel_size=style.LARGE_ICON_SIZE,
                    icon_name='activity-journal',
                    stroke_color=style.COLOR_BUTTON_GREY.get_svg(),
                    fill_color=style.COLOR_TRANSPARENT.get_svg())
        box.pack_start(icon, expand=True, fill=False, padding=0)

        label = Gtk.Label()
        color = style.COLOR_BUTTON_GREY.get_html()
        label.set_markup('<span weight="bold" color="%s">%s</span>' % ( \
                color, glib.markup_escape_text(message)))
        box.pack_start(label, expand=True, fill=False, padding=0)

        if show_clear_query:
            button_box = Gtk.HButtonBox()
            button_box.set_layout(Gtk.ButtonBoxStyle.CENTER)
            box.pack_start(button_box, False, True, 0)
            button_box.show()

            button = Gtk.Button(label=_('Clear search'))
            button.connect('clicked', self.__clear_button_clicked_cb)
            button.props.image = Icon(icon_name='dialog-cancel',
                                      icon_size=Gtk.IconSize.BUTTON)
            button_box.pack_start(button, expand=True, fill=False, padding=0)

        background_box.show_all()

    def __clear_button_clicked_cb(self, button):
        self.emit('clear-clicked')

    def _clear_message(self):
        if self.get_child() == self._scrolled_window:
            return
        self.remove(self.get_child())
        self.add(self._scrolled_window)
        self._scrolled_window.show()

    def update_dates(self):
        if not self.tree_view.get_realized():
            return
        visible_range = self.tree_view.get_visible_range()
        if visible_range is None:
            return

        logging.debug('ListView.update_dates')

        path, end_path = visible_range
        tree_model = self.tree_view.get_model()

        while True:
            cel_rect = self.tree_view.get_cell_area(path,
                                                    self.sort_column)
            x, y = self.tree_view.convert_tree_to_widget_coords(cel_rect.x,
                                                                cel_rect.y)
            self.tree_view.queue_draw_area(x, y, cel_rect.width,
                                           cel_rect.height)
            if path == end_path:
                break
            else:
                next_iter = tree_model.iter_next(tree_model.get_iter(path))
                path = tree_model.get_path(next_iter)

    def _set_dirty(self):
        if self._fully_obscured:
            self._dirty = True
        else:
            self.refresh()

    def set_is_visible(self, visible):
        if visible != self._fully_obscured:
            return

        logging.debug('canvas_visibility_notify_event_cb %r', visible)
        if visible:
            self._fully_obscured = False
            if self._dirty:
                self.refresh()
            if self._update_dates_timer is None:
                logging.debug('Adding date updating timer')
                self._update_dates_timer = \
                        GObject.timeout_add_seconds(UPDATE_INTERVAL,
                                            self.__update_dates_timer_cb)
        else:
            self._fully_obscured = True
            if self._update_dates_timer is not None:
                logging.debug('Remove date updating timer')
                GObject.source_remove(self._update_dates_timer)
                self._update_dates_timer = None

    def __update_dates_timer_cb(self):
        self.update_dates()
        return True
Beispiel #9
0
class BaseListView(Gtk.Bin):
    __gtype_name__ = "JournalBaseListView"

    __gsignals__ = {
        "clear-clicked": (GObject.SignalFlags.RUN_FIRST, None, ([])),
        "selection-changed": (GObject.SignalFlags.RUN_FIRST, None, ([int])),
        "detail-clicked": (GObject.SignalFlags.RUN_FIRST, None, ([object])),
        "volume-error": (GObject.SignalFlags.RUN_FIRST, None, ([str, str])),
    }

    def __init__(self, journalactivity, enable_multi_operations=False):
        self._query = {}
        self._journalactivity = journalactivity
        self._enable_multi_operations = enable_multi_operations
        self._model = None
        self._progress_bar = None
        self._last_progress_bar_pulse = None
        self._scroll_position = 0.0
        self._projects_view_active = False

        Gtk.Bin.__init__(self)

        self.connect("map", self.__map_cb)
        self.connect("unmap", self.__unmap_cb)
        self.connect("destroy", self.__destroy_cb)

        self._scrolled_window = Gtk.ScrolledWindow()
        self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        self.add(self._scrolled_window)
        self._scrolled_window.show()

        self.tree_view = TreeView(self._journalactivity)
        self.tree_view.connect("detail-clicked", self.__detail_clicked_cb)
        self.tree_view.connect("volume-error", self.__volume_error_cb)
        selection = self.tree_view.get_selection()
        selection.set_mode(Gtk.SelectionMode.NONE)
        self.tree_view.props.fixed_height_mode = True
        self._scrolled_window.add(self.tree_view)
        self.tree_view.show()

        self.cell_title = None
        self.cell_icon = None
        self._title_column = None
        self.sort_column = None
        self._scrolling_detector = ScrollingDetector(self._scrolled_window)
        self.tree_view.connect_to_scroller(self._scrolling_detector)

        self._add_columns()
        self.enable_drag_and_copy()

        # Auto-update stuff
        self._fully_obscured = True
        self._updates_disabled = False
        self._dirty = False
        self._refresh_idle_handler = None
        self._update_dates_timer = None
        self._backup_selected = None

        model.created.connect(self.__model_created_cb)
        model.updated.connect(self.__model_updated_cb)
        model.deleted.connect(self.__model_deleted_cb)

    def enable_drag_and_copy(self):
        self.tree_view.drag_source_set(
            Gdk.ModifierType.BUTTON1_MASK,
            [Gtk.TargetEntry.new("text/uri-list", 0, 0), Gtk.TargetEntry.new("journal-object-id", 0, 0)],
            Gdk.DragAction.COPY,
        )

    def disable_drag_and_copy(self):
        self.tree_view.unset_rows_drag_source()

    def __model_created_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_updated_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_deleted_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def _is_new_item_visible(self, object_id):
        """Check if the created item is part of the currently selected view"""
        if "project_id" in self._query:
            # TODO:  Would be best to check if the object_id is in the project.
            #        But there is only ever 1 project listview, so it should
            #        not be very costly.
            return True
        if not self._query.get("mountpoints", None):
            return None
        if self._query["mountpoints"] == ["/"]:
            return not object_id.startswith("/")
        else:
            return object_id.startswith(self._query["mountpoints"][0])

    def _add_columns(self):
        if self._enable_multi_operations:
            cell_select = Gtk.CellRendererToggle()
            cell_select.connect("toggled", self.__cell_select_toggled_cb)
            cell_select.props.activatable = True
            cell_select.props.xpad = style.DEFAULT_PADDING
            cell_select.props.indicator_size = style.zoom(26)

            column = Gtk.TreeViewColumn()
            column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
            column.props.fixed_width = style.GRID_CELL_SIZE
            column.pack_start(cell_select, True)
            column.set_cell_data_func(cell_select, self.__select_set_data_cb)
            self.tree_view.append_column(column)

        cell_favorite = CellRendererFavorite()
        cell_favorite.connect("clicked", self._favorite_clicked_cb)
        cell_favorite.connect_to_scroller(self._scrolling_detector)

        self._fav_column = Gtk.TreeViewColumn()
        self._fav_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self._fav_column.props.fixed_width = cell_favorite.props.width
        self._fav_column.pack_start(cell_favorite, True)
        self._fav_column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb)
        self.tree_view.append_column(self._fav_column)

        self.cell_icon = CellRendererActivityIcon()
        self.cell_icon.connect_to_scroller(self._scrolling_detector)

        column = Gtk.TreeViewColumn()
        self.tree_view.icon_activity_column = column
        column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        column.props.fixed_width = self.cell_icon.props.width
        column.pack_start(self.cell_icon, True)
        column.add_attribute(self.cell_icon, "file-name", ListModel.COLUMN_ICON)
        column.add_attribute(self.cell_icon, "xo-color", ListModel.COLUMN_ICON_COLOR)
        self.tree_view.append_column(column)
        self.icon_activity_column = column

        self.cell_title = Gtk.CellRendererText()
        self.cell_title.props.ellipsize = style.ELLIPSIZE_MODE_DEFAULT
        self.cell_title.props.ellipsize_set = True

        self._title_column = Gtk.TreeViewColumn()
        self._title_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self._title_column.props.expand = True
        self._title_column.props.clickable = True
        self._title_column.pack_start(self.cell_title, True)
        self._title_column.add_attribute(self.cell_title, "markup", ListModel.COLUMN_TITLE)
        self.tree_view.append_column(self._title_column)

        for column_index in [ListModel.COLUMN_BUDDY_1, ListModel.COLUMN_BUDDY_2, ListModel.COLUMN_BUDDY_3]:

            buddies_column = Gtk.TreeViewColumn()
            buddies_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
            self.tree_view.append_column(buddies_column)

            cell_icon = CellRendererBuddy(column_index=column_index)
            buddies_column.pack_start(cell_icon, True)
            buddies_column.props.fixed_width += cell_icon.props.width
            buddies_column.add_attribute(cell_icon, "buddy", column_index)
            buddies_column.set_cell_data_func(cell_icon, self.__buddies_set_data_cb)
            self.tree_view.buddies_columns.append(buddies_column)

        cell_progress = Gtk.CellRendererProgress()
        cell_progress.props.ypad = style.GRID_CELL_SIZE / 4
        buddies_column.pack_start(cell_progress, True)
        buddies_column.add_attribute(cell_progress, "value", ListModel.COLUMN_PROGRESS)
        buddies_column.set_cell_data_func(cell_progress, self.__progress_data_cb)

        cell_text = Gtk.CellRendererText()
        cell_text.props.xalign = 1

        # Measure the required width for a date in the form of "10 hours, 10
        # minutes ago"
        timestamp = time.time() - 10 * 60 - 10 * 60 * 60
        date = util.timestamp_to_elapsed_string(timestamp)
        date_width = self._get_width_for_string(date)

        self.sort_column = Gtk.TreeViewColumn()
        self.sort_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
        self.sort_column.props.fixed_width = date_width
        self.sort_column.set_alignment(1)
        self.sort_column.props.resizable = True
        self.sort_column.props.clickable = True
        self.sort_column.pack_start(cell_text, True)
        self.sort_column.add_attribute(cell_text, "text", ListModel.COLUMN_TIMESTAMP)
        self.tree_view.append_column(self.sort_column)

    def _get_width_for_string(self, text):
        # Add some extra margin
        text = text + "aaaaa"

        widget = Gtk.Label(label="")
        context = widget.get_pango_context()
        layout = Pango.Layout(context)
        layout.set_text(text, len(text))
        width, height_ = layout.get_pixel_size()
        return width

    def do_size_allocate(self, allocation):
        self.set_allocation(allocation)
        self.get_child().size_allocate(allocation)

    def do_size_request(self, requisition):
        requisition.width, requisition.height = self.get_child().size_request()

    def __destroy_cb(self, widget):
        if self._model is not None:
            self._model.stop()

    def __buddies_set_data_cb(self, column, cell, tree_model, tree_iter, data):
        buddy = tree_model.do_get_value(tree_iter, cell._model_column_index)
        if buddy is None:
            cell.props.visible = False
            return
        # FIXME workaround for pygobject bug, see
        # https://bugzilla.gnome.org/show_bug.cgi?id=689277
        #
        # add_attribute with 'buddy' attribute in the cell should take
        # care of setting it.
        cell.props.buddy = buddy

        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress >= 100

    def __progress_data_cb(self, column, cell, tree_model, tree_iter, data):
        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress < 100

    def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter, data):
        favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
        if favorite:
            cell.props.xo_color = profile.get_color()
        else:
            cell.props.xo_color = None

    def _favorite_clicked_cb(self, cell, path):
        row = self._model[path]
        iterator = self._model.get_iter(path)
        metadata = model.get(row[ListModel.COLUMN_UID])
        if not model.is_editable(metadata):
            return
        if metadata.get("keep", 0) == "1":
            metadata["keep"] = "0"
            self._model[iterator][ListModel.COLUMN_FAVORITE] = "0"
        else:
            metadata["keep"] = "1"
            self._model[iterator][ListModel.COLUMN_FAVORITE] = "1"

        cell_rect = self.tree_view.get_cell_area(path, self._fav_column)
        self.tree_view.queue_draw_area(cell_rect.x, cell_rect.y, cell_rect.width, cell_rect.height)

    def __select_set_data_cb(self, column, cell, tree_model, tree_iter, data):
        uid = tree_model[tree_iter][ListModel.COLUMN_UID]
        if uid is None:
            return
        cell.props.active = self._model.is_selected(uid)

    def __cell_select_toggled_cb(self, cell, path):
        tree_iter = self._model.get_iter(path)
        uid = self._model[tree_iter][ListModel.COLUMN_UID]
        self._model.set_selected(uid, not cell.get_active())
        self.emit("selection-changed", len(self._model.get_selected_items()))

    def update_with_query(self, query_dict):
        logging.debug("ListView.update_with_query")

        if "order_by" not in query_dict:
            query_dict["order_by"] = ["+timestamp"]
        if query_dict["order_by"] != self._query.get("order_by"):
            property_ = query_dict["order_by"][0][1:]
            cell_text = self.sort_column.get_cells()[0]
            self.sort_column.set_attributes(
                cell_text, text=getattr(ListModel, "COLUMN_" + property_.upper(), ListModel.COLUMN_TIMESTAMP)
            )
        self._query = query_dict
        self.refresh(new_query=True)

    def refresh(self, new_query=False):
        logging.debug("ListView.refresh query %r", self._query)
        self._stop_progress_bar()
        window = self.get_toplevel().get_window()
        if window is not None:
            window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
        GObject.idle_add(self._do_refresh, new_query)

    def _do_refresh(self, new_query=False):
        if self._model is not None:
            if new_query:
                self._backup_selected = None
            else:
                self._backup_selected = self._model.get_selected_items()
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect("ready", self.__model_ready_cb)
        self._model.connect("progress", self.__model_progress_cb)
        self._model.setup(self.__model_updated_cb)
        window = self.get_toplevel().get_window()
        if window is not None:
            window.set_cursor(None)

    def __model_ready_cb(self, tree_model):
        self._stop_progress_bar()

        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug("ListView.__model_ready_cb %r", self._scroll_position)

        x11_window = self.tree_view.get_window()

        if x11_window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().hide()

        # if the selection was preserved, restore it
        if self._backup_selected is not None:
            tree_model.restore_selection(self._backup_selected)
            self.emit("selection-changed", len(self._backup_selected))

        # Cannot set it up earlier because will try to access the model
        # and it needs to be ready.
        self.tree_view.set_model(self._model)

        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()

        if x11_window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().show()

        if len(tree_model) == 0:
            if self._query.get("project_id", None):
                self._show_message(_("Your project is empty"))
            else:
                documents_path = model.get_documents_path()
                if self._is_query_empty():
                    if self._query["mountpoints"] == ["/"]:
                        self._show_message(_("Your Journal is empty"))
                    elif documents_path and self._query["mountpoints"] == [documents_path]:
                        self._show_message(_("Your documents folder is empty"))
                    else:
                        self._show_message(_("The device is empty"))
                else:
                    show_message_text = "No matching entries"
                    if self.get_projects_view_active():
                        show_message_text = "No Projects"

                self._show_message(_(show_message_text), show_clear_query=self._can_clear_query())
        else:
            self._clear_message()

    def _can_clear_query(self):
        return True

    def __map_cb(self, widget):
        logging.debug("ListView.__map_cb %r", self._scroll_position)
        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()
        self.set_is_visible(True)

    def __unmap_cb(self, widget):
        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug("ListView.__unmap_cb %r", self._scroll_position)
        self.set_is_visible(False)

    def _is_query_empty(self):
        # FIXME: This is a hack, we shouldn't have to update this every time
        # a new search term is added.
        return not (
            self._query.get("query")
            or self._query.get("mime_type")
            or self._query.get("keep")
            or self._query.get("mtime")
            or self._query.get("activity")
        )

    def __model_progress_cb(self, tree_model):
        if self._progress_bar is None:
            self._start_progress_bar()

        if time.time() - self._last_progress_bar_pulse > 0.05:
            self._progress_bar.pulse()
            self._last_progress_bar_pulse = time.time()

    def _start_progress_bar(self):
        alignment = Gtk.Alignment.new(xalign=0.5, yalign=0.5, xscale=0.5, yscale=0)
        self.remove(self.get_child())
        self.add(alignment)
        alignment.show()

        self._progress_bar = Gtk.ProgressBar()
        self._progress_bar.props.pulse_step = 0.01
        self._last_progress_bar_pulse = time.time()
        alignment.add(self._progress_bar)
        self._progress_bar.show()

    def _stop_progress_bar(self):
        if self._progress_bar is None:
            return
        self.remove(self.get_child())
        self.add(self._scrolled_window)
        self._progress_bar = None

    def _show_message(self, message, show_clear_query=False):
        self.remove(self.get_child())

        background_box = Gtk.EventBox()
        background_box.modify_bg(Gtk.StateType.NORMAL, style.COLOR_WHITE.get_gdk_color())
        self.add(background_box)

        alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1)
        background_box.add(alignment)

        box = Gtk.VBox()
        alignment.add(box)

        icon = Icon(
            pixel_size=style.LARGE_ICON_SIZE,
            icon_name="activity-journal",
            stroke_color=style.COLOR_BUTTON_GREY.get_svg(),
            fill_color=style.COLOR_TRANSPARENT.get_svg(),
        )
        box.pack_start(icon, expand=True, fill=False, padding=0)

        label = Gtk.Label()
        color = style.COLOR_BUTTON_GREY.get_html()
        label.set_markup('<span weight="bold" color="%s">%s</span>' % (color, GLib.markup_escape_text(message)))
        box.pack_start(label, expand=True, fill=False, padding=0)

        if not self.get_projects_view_active():
            if show_clear_query:
                button_box = Gtk.HButtonBox()
                button_box.set_layout(Gtk.ButtonBoxStyle.CENTER)
                box.pack_start(button_box, False, True, 0)
                button_box.show()

                button = Gtk.Button(label=_("Clear search"))
                button.connect("clicked", self.__clear_button_clicked_cb)
                button.props.image = Icon(icon_name="dialog-cancel", pixel_size=style.SMALL_ICON_SIZE)
                button_box.pack_start(button, expand=True, fill=False, padding=0)

        background_box.show_all()

    def __clear_button_clicked_cb(self, button):
        self.emit("clear-clicked")

    def _clear_message(self):
        if self.get_child() == self._scrolled_window:
            return
        self.remove(self.get_child())
        self.add(self._scrolled_window)
        self._scrolled_window.show()

    def update_dates(self):
        if not self.tree_view.get_realized():
            return
        visible_range = self.tree_view.get_visible_range()
        if visible_range is None:
            return

        logging.debug("ListView.update_dates")

        path, end_path = visible_range
        tree_model = self.tree_view.get_model()

        while True:
            cel_rect = self.tree_view.get_cell_area(path, self.sort_column)
            x, y = self.tree_view.convert_tree_to_widget_coords(cel_rect.x, cel_rect.y)
            self.tree_view.queue_draw_area(x, y, cel_rect.width, cel_rect.height)
            if path == end_path:
                break
            else:
                next_iter = tree_model.iter_next(tree_model.get_iter(path))
                path = tree_model.get_path(next_iter)

    def _set_dirty(self):
        if self._fully_obscured or self._updates_disabled:
            self._dirty = True
        else:
            self.refresh()

    def disable_updates(self):
        self._updates_disabled = True

    def enable_updates(self):
        self._updates_disabled = False
        if self._dirty:
            self.refresh()

    def set_is_visible(self, visible):
        if visible != self._fully_obscured:
            return

        logging.debug("canvas_visibility_notify_event_cb %r", visible)
        if visible:
            self._fully_obscured = False
            if self._dirty:
                self.refresh()
            if self._update_dates_timer is None:
                logging.debug("Adding date updating timer")
                self._update_dates_timer = GObject.timeout_add_seconds(UPDATE_INTERVAL, self.__update_dates_timer_cb)
        else:
            self._fully_obscured = True
            if self._update_dates_timer is not None:
                logging.debug("Remove date updating timer")
                GObject.source_remove(self._update_dates_timer)
                self._update_dates_timer = None

    def __update_dates_timer_cb(self):
        self.update_dates()
        return True

    def get_model(self):
        return self._model

    def select_all(self):
        self.get_model().select_all()
        self.tree_view.queue_draw()
        self.emit("selection-changed", len(self._model.get_selected_items()))

    def select_none(self):
        self.get_model().select_none()
        self.tree_view.queue_draw()
        self.emit("selection-changed", len(self._model.get_selected_items()))

    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 get_projects_view_active(self):
        return self._query.get("activity") == PROJECT_BUNDLE_ID
Beispiel #10
0
class BaseListView(gtk.Bin):
    __gtype_name__ = 'JournalBaseListView'

    __gsignals__ = {
        'clear-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
    }

    def __init__(self):
        self._query = {}
        self._model = None
        self._progress_bar = None
        self._last_progress_bar_pulse = None
        self._scroll_position = 0.

        gobject.GObject.__init__(self)

        self.connect('map', self.__map_cb)
        self.connect('unrealize', self.__unrealize_cb)
        self.connect('destroy', self.__destroy_cb)

        self._scrolled_window = gtk.ScrolledWindow()
        self._scrolled_window.set_policy(gtk.POLICY_NEVER,
                                         gtk.POLICY_AUTOMATIC)
        self.add(self._scrolled_window)
        self._scrolled_window.show()

        self.tree_view = TreeView()
        selection = self.tree_view.get_selection()
        selection.set_mode(gtk.SELECTION_NONE)
        self.tree_view.props.fixed_height_mode = True
        self.tree_view.modify_base(gtk.STATE_NORMAL,
                                   style.COLOR_WHITE.get_gdk_color())
        self._scrolled_window.add(self.tree_view)
        self.tree_view.show()

        self.cell_title = None
        self.cell_icon = None
        self._title_column = None
        self.sort_column = None
        self._add_columns()

        self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
                                                [('text/uri-list', 0, 0),
                                                 ('journal-object-id', 0, 0)],
                                                gtk.gdk.ACTION_COPY)

        # Auto-update stuff
        self._fully_obscured = True
        self._dirty = False
        self._refresh_idle_handler = None
        self._update_dates_timer = None

        model.created.connect(self.__model_created_cb)
        model.updated.connect(self.__model_updated_cb)
        model.deleted.connect(self.__model_deleted_cb)

    def __model_created_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_updated_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def __model_deleted_cb(self, sender, signal, object_id):
        if self._is_new_item_visible(object_id):
            self._set_dirty()

    def _is_new_item_visible(self, object_id):
        """Check if the created item is part of the currently selected view"""
        if self._query['mountpoints'] == ['/']:
            return not object_id.startswith('/')
        else:
            return object_id.startswith(self._query['mountpoints'][0])

    def _add_columns(self):
        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
        column.pack_start(cell_favorite)
        column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb)
        self.tree_view.append_column(column)

        self.cell_icon = CellRendererActivityIcon(self.tree_view)

        column = gtk.TreeViewColumn()
        column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
        column.props.fixed_width = self.cell_icon.props.width
        column.pack_start(self.cell_icon)
        column.add_attribute(self.cell_icon, 'file-name',
                             ListModel.COLUMN_ICON)
        column.add_attribute(self.cell_icon, 'xo-color',
                             ListModel.COLUMN_ICON_COLOR)
        self.tree_view.append_column(column)

        self.cell_title = gtk.CellRendererText()
        self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE
        self.cell_title.props.ellipsize_set = True

        self._title_column = gtk.TreeViewColumn()
        self._title_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
        self._title_column.props.expand = True
        self._title_column.props.clickable = True
        self._title_column.pack_start(self.cell_title)
        self._title_column.add_attribute(self.cell_title, 'markup',
                                         ListModel.COLUMN_TITLE)
        self.tree_view.append_column(self._title_column)

        buddies_column = gtk.TreeViewColumn()
        buddies_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
        self.tree_view.append_column(buddies_column)

        for column_index in [
                ListModel.COLUMN_BUDDY_1, ListModel.COLUMN_BUDDY_2,
                ListModel.COLUMN_BUDDY_3
        ]:
            cell_icon = CellRendererBuddy(self.tree_view,
                                          column_index=column_index)
            buddies_column.pack_start(cell_icon)
            buddies_column.props.fixed_width += cell_icon.props.width
            buddies_column.add_attribute(cell_icon, 'buddy', column_index)
            buddies_column.set_cell_data_func(cell_icon,
                                              self.__buddies_set_data_cb)

        cell_progress = gtk.CellRendererProgress()
        cell_progress.props.ypad = style.GRID_CELL_SIZE / 4
        buddies_column.pack_start(cell_progress)
        buddies_column.add_attribute(cell_progress, 'value',
                                     ListModel.COLUMN_PROGRESS)
        buddies_column.set_cell_data_func(cell_progress,
                                          self.__progress_data_cb)

        cell_text = gtk.CellRendererText()
        cell_text.props.xalign = 1

        # Measure the required width for a date in the form of "10 hours, 10
        # minutes ago"
        timestamp = time.time() - 10 * 60 - 10 * 60 * 60
        date = util.timestamp_to_elapsed_string(timestamp)
        date_width = self._get_width_for_string(date)

        self.sort_column = gtk.TreeViewColumn()
        self.sort_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
        self.sort_column.props.fixed_width = date_width
        self.sort_column.set_alignment(1)
        self.sort_column.props.resizable = True
        self.sort_column.props.clickable = True
        self.sort_column.pack_start(cell_text)
        self.sort_column.add_attribute(cell_text, 'text',
                                       ListModel.COLUMN_TIMESTAMP)
        self.tree_view.append_column(self.sort_column)

    def _get_width_for_string(self, text):
        # Add some extra margin
        text = text + 'aaaaa'

        widget = gtk.Label('')
        context = widget.get_pango_context()
        layout = pango.Layout(context)
        layout.set_text(text)
        width, height_ = layout.get_size()
        return pango.PIXELS(width)

    def do_size_allocate(self, allocation):
        self.allocation = allocation
        self.child.size_allocate(allocation)

    def do_size_request(self, requisition):
        requisition.width, requisition.height = self.child.size_request()

    def __destroy_cb(self, widget):
        if self._model is not None:
            self._model.stop()

    def __buddies_set_data_cb(self, column, cell, tree_model, tree_iter):
        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress >= 100

    def __progress_data_cb(self, column, cell, tree_model, tree_iter):
        progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
        cell.props.visible = progress < 100

    def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter):
        favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
        if favorite:
            client = gconf.client_get_default()
            color = XoColor(client.get_string('/desktop/sugar/user/color'))
            cell.props.xo_color = color
        else:
            cell.props.xo_color = None

    def __favorite_clicked_cb(self, cell, path):
        row = self._model[path]
        metadata = model.get(row[ListModel.COLUMN_UID])
        if not model.is_editable(metadata):
            return
        if metadata.get('keep', 0) == '1':
            metadata['keep'] = '0'
        else:
            metadata['keep'] = '1'
        model.write(metadata, update_mtime=False)

    def update_with_query(self, query_dict):
        logging.debug('ListView.update_with_query')
        if 'order_by' not in query_dict:
            query_dict['order_by'] = ['+timestamp']
        if query_dict['order_by'] != self._query.get('order_by'):
            property_ = query_dict['order_by'][0][1:]
            cell_text = self.sort_column.get_cell_renderers()[0]
            self.sort_column.set_attributes(
                cell_text,
                text=getattr(ListModel, 'COLUMN_' + property_.upper(),
                             ListModel.COLUMN_TIMESTAMP))
        self._query = query_dict

        self.refresh()

    def refresh(self):
        logging.debug('ListView.refresh query %r', self._query)
        self._stop_progress_bar()

        if self._model is not None:
            self._model.stop()
        self._dirty = False

        self._model = ListModel(self._query)
        self._model.connect('ready', self.__model_ready_cb)
        self._model.connect('progress', self.__model_progress_cb)
        self._model.setup()

    def __model_ready_cb(self, tree_model):
        self._stop_progress_bar()

        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug('ListView.__model_ready_cb %r', self._scroll_position)

        if self.tree_view.window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().hide()

        # Cannot set it up earlier because will try to access the model
        # and it needs to be ready.
        self.tree_view.set_model(self._model)

        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()

        if self.tree_view.window is not None:
            # prevent glitches while later vadjustment setting, see #1235
            self.tree_view.get_bin_window().show()

        if len(tree_model) == 0:
            if self._is_query_empty():
                if self._query['mountpoints'] == ['/']:
                    self._show_message(_('Your Journal is empty'))
                elif self._query['mountpoints'] == \
                        [model.get_documents_path()]:
                    self._show_message(_('Your documents folder is empty'))
                else:
                    self._show_message(_('The device is empty'))
            else:
                self._show_message(_('No matching entries'),
                                   show_clear_query=True)
        else:
            self._clear_message()

    def __map_cb(self, widget):
        logging.debug('ListView.__map_cb %r', self._scroll_position)
        self.tree_view.props.vadjustment.props.value = self._scroll_position
        self.tree_view.props.vadjustment.value_changed()

    def __unrealize_cb(self, widget):
        self._scroll_position = self.tree_view.props.vadjustment.props.value
        logging.debug('ListView.__map_cb %r', self._scroll_position)

    def _is_query_empty(self):
        # FIXME: This is a hack, we shouldn't have to update this every time
        # a new search term is added.
        return not (self._query.get('query') or self._query.get('mime_type')
                    or self._query.get('keep') or self._query.get('mtime')
                    or self._query.get('activity'))

    def __model_progress_cb(self, tree_model):
        if self._progress_bar is None:
            self._start_progress_bar()

        if time.time() - self._last_progress_bar_pulse > 0.05:
            self._progress_bar.pulse()
            self._last_progress_bar_pulse = time.time()

    def _start_progress_bar(self):
        alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5)
        self.remove(self.child)
        self.add(alignment)
        alignment.show()

        self._progress_bar = gtk.ProgressBar()
        self._progress_bar.props.pulse_step = 0.01
        self._last_progress_bar_pulse = time.time()
        alignment.add(self._progress_bar)
        self._progress_bar.show()

    def _stop_progress_bar(self):
        if self._progress_bar is None:
            return
        self.remove(self.child)
        self.add(self._scrolled_window)
        self._progress_bar = None

    def _show_message(self, message, show_clear_query=False):
        canvas = hippo.Canvas()
        self.remove(self.child)
        self.add(canvas)
        canvas.show()

        box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
                              background_color=style.COLOR_WHITE.get_int(),
                              yalign=hippo.ALIGNMENT_CENTER,
                              spacing=style.DEFAULT_SPACING,
                              padding_bottom=style.GRID_CELL_SIZE)
        canvas.set_root(box)

        icon = CanvasIcon(size=style.LARGE_ICON_SIZE,
                          icon_name='activity-journal',
                          stroke_color=style.COLOR_BUTTON_GREY.get_svg(),
                          fill_color=style.COLOR_TRANSPARENT.get_svg())
        box.append(icon)

        text = hippo.CanvasText(text=message,
                                xalign=hippo.ALIGNMENT_CENTER,
                                font_desc=style.FONT_BOLD.get_pango_desc(),
                                color=style.COLOR_BUTTON_GREY.get_int())
        box.append(text)

        if show_clear_query:
            button = gtk.Button(label=_('Clear search'))
            button.connect('clicked', self.__clear_button_clicked_cb)
            button.props.image = Icon(icon_name='dialog-cancel',
                                      icon_size=gtk.ICON_SIZE_BUTTON)
            canvas_button = hippo.CanvasWidget(widget=button,
                                               xalign=hippo.ALIGNMENT_CENTER)
            box.append(canvas_button)

    def __clear_button_clicked_cb(self, button):
        self.emit('clear-clicked')

    def _clear_message(self):
        if self.child == self._scrolled_window:
            return
        self.remove(self.child)
        self.add(self._scrolled_window)
        self._scrolled_window.show()

    def update_dates(self):
        if not self.tree_view.flags() & gtk.REALIZED:
            return
        visible_range = self.tree_view.get_visible_range()
        if visible_range is None:
            return

        logging.debug('ListView.update_dates')

        path, end_path = visible_range
        tree_model = self.tree_view.get_model()

        while True:
            x, y, width, height = self.tree_view.get_cell_area(
                path, self.sort_column)
            x, y = self.tree_view.convert_tree_to_widget_coords(x, y)
            self.tree_view.queue_draw_area(x, y, width, height)
            if path == end_path:
                break
            else:
                next_iter = tree_model.iter_next(tree_model.get_iter(path))
                path = tree_model.get_path(next_iter)

    def _set_dirty(self):
        if self._fully_obscured:
            self._dirty = True
        else:
            self.refresh()

    def set_is_visible(self, visible):
        if visible != self._fully_obscured:
            return

        logging.debug('canvas_visibility_notify_event_cb %r', visible)
        if visible:
            self._fully_obscured = False
            if self._dirty:
                self.refresh()
            if self._update_dates_timer is None:
                logging.debug('Adding date updating timer')
                self._update_dates_timer = \
                        gobject.timeout_add_seconds(UPDATE_INTERVAL,
                                            self.__update_dates_timer_cb)
        else:
            self._fully_obscured = True
            if self._update_dates_timer is not None:
                logging.debug('Remove date updating timer')
                gobject.source_remove(self._update_dates_timer)
                self._update_dates_timer = None

    def __update_dates_timer_cb(self):
        self.update_dates()
        return True