diff --git a/rigo/RigoDaemon/app.py b/rigo/RigoDaemon/app.py index 3798bab5d..3493c4011 100755 --- a/rigo/RigoDaemon/app.py +++ b/rigo/RigoDaemon/app.py @@ -1891,8 +1891,12 @@ class RigoDaemonService(dbus.service.Object): self._close_local_resources() self._entropy_setup() - unavailable_repositories = \ - self._entropy.unavailable_repositories() + self._rwsem.reader_acquire() + try: + unavailable_repositories = \ + self._entropy.unavailable_repositories() + finally: + self._rwsem.reader_release() if unavailable_repositories: GLib.idle_add( self.unavailable_repositories, @@ -1901,27 +1905,18 @@ class RigoDaemonService(dbus.service.Object): if Repository.are_repositories_old(): GLib.idle_add(self.old_repositories) - # signal updates available - update, remove, fine, spm_fine = \ - self._entropy.calculate_updates() - if update or remove: - GLib.idle_add(self.updates_available, - update, remove) + self._rwsem.reader_acquire() + try: + # signal updates available + update, remove, fine, spm_fine = \ + self._entropy.calculate_updates() + if update or remove: + GLib.idle_add(self.updates_available, + update, remove) + finally: + self._rwsem.reader_release() - notices = [] - for repository in self._entropy.repositories(): - notice = self._entropy.get_noticeboard( - repository) - if not notice: - continue - notices.append((repository, notice)) - - notices = \ - self._dbus_prepare_noticeboard_metadata( - notices) - if notices: - GLib.idle_add(self.noticeboards_available, - notices) + self._maybe_signal_noticeboards_available_unlocked() finally: self._release_shared() @@ -1929,6 +1924,45 @@ class RigoDaemonService(dbus.service.Object): if acquired: self._greetings_serializer.release() + def _maybe_signal_noticeboards_available_unlocked(self): + """ + Unlocked version (no shared nor exclusive Entropy + Resources Lock acquired, no activity mutex acquired) + of _maybe_signal_noticeboards_available() + """ + self._rwsem.reader_acquire() + try: + notices = [] + for repository in self._entropy.repositories(): + notice = self._entropy.get_noticeboard( + repository) + if not notice: + continue + notices.append((repository, notice)) + finally: + self._rwsem.reader_release() + + if notices: + GLib.idle_add( + self.noticeboards_available, + self._dbus_prepare_noticeboard_metadata( + notices) + ) + + def _maybe_signal_noticeboards_available(self): + """ + Signal (as soon as RigoDaemon can) the availability + of NoticeBoards among configured repositories. + """ + with self._activity_mutex: + self._acquire_shared() + try: + self._close_local_resources() + self._entropy_setup() + self._maybe_signal_noticeboards_available_unlocked() + finally: + self._release_shared() + def _dbus_prepare_noticeboard_metadata(self, notices): """ Prepare Notice Board repositories metadata for sending @@ -2381,8 +2415,8 @@ class RigoDaemonService(dbus.service.Object): out_signature='', sender_keyword='sender') def configuration_updates(self, sender=None): """ - Return the last generated (if any, or create a new one) - ConfigurationFiles object. + Request RigoDaemon to signal for configuration file + updates, if any. """ write_output("configuration_updates called", debug=True) def _signal(pid): @@ -2399,6 +2433,21 @@ class RigoDaemonService(dbus.service.Object): task.daemon = True task.start() + @dbus.service.method(BUS_NAME, in_signature='', + out_signature='') + def noticeboards(self): + """ + Request RigoDaemon to signal for noticeboards + data, if any. + """ + write_output("noticeboards called", debug=True) + + task = ParallelTask( + self._maybe_signal_noticeboards_available) + task.name = "NoticeboardsAvailableSignal" + task.daemon = True + task.start() + @dbus.service.method(BUS_NAME, in_signature='', out_signature='', sender_keyword='sender') def reload_configuration_updates(self, sender=None): diff --git a/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml b/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml index 924894dc0..7747229b6 100644 --- a/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml +++ b/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml @@ -67,6 +67,8 @@ + + diff --git a/rigo/data/ui/gtk3/rigo.ui b/rigo/data/ui/gtk3/rigo.ui index e72ef6e38..d3ac51acf 100644 --- a/rigo/data/ui/gtk3/rigo.ui +++ b/rigo/data/ui/gtk3/rigo.ui @@ -162,6 +162,32 @@ 4 + + + False + + + True + True + never + automatic + + + + + + True + True + 0 + + + + + True + True + 5 + + True @@ -547,7 +573,7 @@ True True - 5 + 6 diff --git a/rigo/rigo/controllers/daemon.py b/rigo/rigo/controllers/daemon.py index 76827a7f1..cf295520e 100644 --- a/rigo/rigo/controllers/daemon.py +++ b/rigo/rigo/controllers/daemon.py @@ -31,6 +31,7 @@ from rigo.enums import AppActions, RigoViewStates, \ LocalActivityStates from rigo.models.application import Application from rigo.models.configupdate import ConfigUpdate +from rigo.models.noticeboard import Notice from rigo.ui.gtk3.widgets.notifications import NotificationBox, \ PleaseWaitNotificationBox, LicensesNotificationBox, \ OrphanedAppsNotificationBox, InstallNotificationBox, \ @@ -176,6 +177,7 @@ class RigoServiceController(GObject.Object): _UPDATES_AVAILABLE_SIGNAL = "updates_available" _UNAVAILABLE_REPOSITORIES_SIGNAL = "unavailable_repositories" _OLD_REPOSITORIES_SIGNAL = "old_repositories" + _NOTICEBOARDS_AVAILABLE_SIGNAL = "noticeboards_available" _SUPPORTED_APIS = [2] def __init__(self, rigo_app, activity_rwsem, @@ -185,6 +187,7 @@ class RigoServiceController(GObject.Object): self._activity_rwsem = activity_rwsem self._nc = None self._confc = None + self._notc = None self._bottom_nc = None self._wc = None self._avc = None @@ -252,6 +255,12 @@ class RigoServiceController(GObject.Object): """ self._confc = confc + def set_noticeboard_controller(self, notc): + """ + Bind a NoticeBoardViewController object to this class. + """ + self._notc = notc + def set_terminal(self, terminal): """ Bind a TerminalWidget to this object, in order to be used with @@ -511,6 +520,12 @@ class RigoServiceController(GObject.Object): self._old_repositories_signal, dbus_interface=self.DBUS_INTERFACE) + # RigoDaemon tells us that noticeboards are available + self.__entropy_bus.connect_to_signal( + self._NOTICEBOARDS_AVAILABLE_SIGNAL, + self._noticeboards_available_signal, + dbus_interface=self.DBUS_INTERFACE) + return self.__entropy_bus ### GOBJECT EVENTS @@ -833,6 +848,25 @@ class RigoServiceController(GObject.Object): box.connect("update-request", _on_update) self._nc.append(box) + def _noticeboards_available_signal(self, notices): + const_debug_write( + __name__, + "_noticeboards_available_signal: called") + if self._nc is not None and self._notc is not None: + notice_boards = [] + for repository, notice_id, guid, link, title, desc, date in notices: + data = { + 'guid': guid, + 'link': link, + 'title': title, + 'description': desc, + 'pubDate': date + } + nb = Notice(repository, notice_id, data) + notice_boards.append(nb) + if notice_boards: + self._notc.notify_notices(notice_boards) + def _old_repositories_signal(self): const_debug_write( __name__, @@ -1355,6 +1389,17 @@ class RigoServiceController(GObject.Object): ).configuration_updates() return self._execute_mainloop(_config) + def noticeboards(self): + """ + Request Repositories NoticeBoards. + """ + def _notice(): + dbus.Interface( + self._entropy_bus, + dbus_interface=self.DBUS_INTERFACE + ).noticeboards() + return self._execute_mainloop(_notice) + def merge_configuration(self, source, reply_handler=None, error_handler=None): """ diff --git a/rigo/rigo/enums.py b/rigo/rigo/enums.py index 2d5b17354..a09916fb0 100644 --- a/rigo/rigo/enums.py +++ b/rigo/rigo/enums.py @@ -58,7 +58,8 @@ class RigoViewStates: APPLICATION_VIEW_STATE, WORK_VIEW_STATE, CONFUPDATES_VIEW_STATE, - ) = range(5) + NOTICEBOARD_VIEW_STATE, + ) = range(6) class LocalActivityStates: ( diff --git a/rigo/rigo/models/noticeboard.py b/rigo/rigo/models/noticeboard.py new file mode 100644 index 000000000..9249c2abf --- /dev/null +++ b/rigo/rigo/models/noticeboard.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2012 Fabio Erculiani + +Authors: + Fabio Erculiani + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +import hashlib +import email.utils + +from rigo.utils import prepare_markup + + +class Notice(object): + + def __init__(self, repository, notice_id, metadata): + self._repository = repository + self._notice_id = notice_id + self._metadata = metadata + + def __str__(self): + """ + String representation of Notice object + """ + return "Notice{%s, %s, %s}" % ( + self._repository, self._notice_id, + self._metadata) + + def repository(self): + """ + Return the Repository name from where this Notice is coming. + """ + return self._repository + + def notice_id(self): + """ + Return the Notice identifier (it's unique among Notice objects + coming from the same NoticeBoard). + """ + return self._notice_id + + def date(self): + """ + Return Notice date (string representation, RFC822). + """ + return self._metadata['pubDate'] + + def parsed_date(self): + """ + Parse Notice date using basing on RFC822 and return + tuple that can be used as sort key. + """ + return email.utils.parsedate_tz(self.date()) + + def description(self): + """ + Return Notice description string. + """ + return self._metadata['description'] + + def title(self): + """ + Return Notice title string. + """ + return self._metadata['title'] + + def link(self): + """ + Return Notice link string. + """ + return self._metadata['link'] + + def guid(self): + """ + Return Notice guid (as in RSS guid) string. + """ + return self._metadata['guid'] + + def hash(self): + """ + Return a stringy hash + """ + m = hashlib.md5() + m.update(self.repository() + "|") + m.update("%s|" % (self.notice_id(),)) + m.update(self.date() + "|") + m.update(self.description() + "|") + m.update(self.title() + "|") + m.update(self.link() + "|") + m.update(self.guid()) + return m.hexdigest() + + def get_markup(self): + """ + Return ConfigurationUpdate markup text. + """ + msg = "%s\n%s, " + \ + "%s\n%s\n\n%s" + msg = msg % ( + self.title(), self.repository(), + self.date(), self.link(), self.description()) + return prepare_markup(msg) diff --git a/rigo/rigo/ui/gtk3/controllers/applications.py b/rigo/rigo/ui/gtk3/controllers/applications.py index 98302eb64..dd4990bc1 100644 --- a/rigo/rigo/ui/gtk3/controllers/applications.py +++ b/rigo/rigo/ui/gtk3/controllers/applications.py @@ -330,6 +330,9 @@ class ApplicationsViewController(GObject.Object): elif text == "rigo:confupdate": self._service.configuration_updates() return + elif text == "rigo:notice": + self._service.noticeboards() + return # debug, simulation elif text == "rigo:vte": diff --git a/rigo/rigo/ui/gtk3/controllers/noticeboard.py b/rigo/rigo/ui/gtk3/controllers/noticeboard.py new file mode 100644 index 000000000..2734b9edf --- /dev/null +++ b/rigo/rigo/ui/gtk3/controllers/noticeboard.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2012 Fabio Erculiani + +Authors: + Fabio Erculiani + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +import os +import hashlib +import errno + +from gi.repository import GObject, GLib + +from rigo.paths import CONF_DIR +from rigo.ui.gtk3.widgets.notifications import \ + NoticeBoardNotificationBox +from rigo.enums import RigoViewStates + +from entropy.cache import EntropyCacher + + +class NoticeBoardViewController(GObject.Object): + + LAST_NOTICES_DIR = os.path.join(CONF_DIR, "last_notices") + LAST_NOTICES_CACHE_KEY = "last_hash" + + def __init__(self, notice_store, notice_view): + GObject.Object.__init__(self) + self._store = notice_store + self._view = notice_view + self._cacher = EntropyCacher() + self._nc = None + self._avc = None + + def _ensure_cache_dir(self): + """ + Make sure the cache directory is available. + """ + path = self.LAST_NOTICES_DIR + try: + os.makedirs(path) + except OSError as err: + if err.errno == errno.EEXIST: + if os.path.isfile(path): + os.remove(path) # fail, yeah + return + elif err.errno == errno.ENOTDIR: + # wtf? we will fail later for sure + return + elif err.errno == errno.EPERM: + # meh! + return + raise + + def _load_last_hash(self): + """ + Return last notices hash. + """ + self._ensure_cache_dir() + data = self._cacher.pop( + self.LAST_NOTICES_CACHE_KEY, + cache_dir=self.LAST_NOTICES_DIR) + return data + + def _store_last_hash(self, last_hash): + """ + Store the last notices hash to disk. + """ + self._ensure_cache_dir() + self._cacher.save( + self.LAST_NOTICES_CACHE_KEY, + last_hash, + cache_dir=self.LAST_NOTICES_DIR) + + def _hash(self, notices): + """ + Hash a list of Notice objects + """ + m = hashlib.md5() + m.update("") + for notice in notices: + m.update(notice.hash()) + return m.hexdigest() + + def setup(self): + """ + Setup the ConfigUpdatesViewController resources. + """ + self._view.set_model(self._store) + self._view.show() + + def set_notification_controller(self, nc): + """ + Bind a UpperNotificationViewController to this class. + """ + self._nc = nc + + def set_applications_controller(self, avc): + """ + Bind an ApplicationsViewController object to this class. + """ + self._avc = avc + + def clear(self): + """ + Clear Configuration Updates + """ + self._view.clear_model() + + def append(self, opaque): + """ + Add a ConfigUpdate object to the store. + """ + self._store.append([opaque]) + + def append_many(self, opaque_list): + """ + Append many ConfigUpdate objects to the store. + """ + for opaque in opaque_list: + self._store.append([opaque]) + + def set_many(self, opaque_list, _from_search=None): + """ + Set a new list of ConfigUpdate objects on the store. + """ + self._view.clear_model() + self.append_many(opaque_list) + + def clear_safe(self): + """ + Thread-safe version of clear() + """ + GLib.idle_add(self.clear) + + def append_safe(self, opaque): + """ + Thread-safe version of append() + """ + GLib.idle_add(self.append, opaque) + + def append_many_safe(self, opaque_list): + """ + Thread-safe version of append_many() + """ + GLib.idle_add(self.append_many, opaque_list) + + def set_many_safe(self, opaque_list): + """ + Thread-safe version of set_many() + """ + GLib.idle_add(self.set_many, opaque_list) + + def notify_notices(self, notices, force=False): + """ + Notify Configuration File Updates to User. + """ + if self._nc is not None and self._avc is not None: + + # sort by date + notices = sorted(notices, key=lambda x: x.parsed_date(), + reverse=True) + + current_hash = self._hash(notices) + last_hash = self._load_last_hash() + if current_hash == last_hash and not force: + return + + self.set_many(notices) + + def _nb_let_me_see(widget): + self._avc.emit( + "view-want-change", + RigoViewStates.NOTICEBOARD_VIEW_STATE, + None) + def _nb_stop_annoying(widget): + self._nc.remove(widget) + self._store_last_hash(current_hash) + + box = NoticeBoardNotificationBox(self._avc, len(notices)) + box.connect("let-me-see", _nb_let_me_see) + box.connect("stop-annoying", _nb_stop_annoying) + self._nc.append(box) diff --git a/rigo/rigo/ui/gtk3/models/noticeboardliststore.py b/rigo/rigo/ui/gtk3/models/noticeboardliststore.py new file mode 100644 index 000000000..3259b8b9e --- /dev/null +++ b/rigo/rigo/ui/gtk3/models/noticeboardliststore.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2012 Fabio Erculiani + +Authors: + Fabio Erculiani + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +from gi.repository import Gtk, GObject, GLib + + +class NoticeBoardListStore(Gtk.ListStore): + + # NoticeBoard object + COL_TYPES = (GObject.TYPE_PYOBJECT,) + + ICON_SIZE = 48 + + __gsignals__ = { + # Redraw signal, requesting UI update + "redraw-request" : (GObject.SignalFlags.RUN_LAST, + None, + tuple(), + ), + } + + def __init__(self): + Gtk.ListStore.__init__(self) + self.set_column_types(self.COL_TYPES) + diff --git a/rigo/rigo/ui/gtk3/widgets/apptreeview.py b/rigo/rigo/ui/gtk3/widgets/apptreeview.py index 4cd591184..87e4ba876 100644 --- a/rigo/rigo/ui/gtk3/widgets/apptreeview.py +++ b/rigo/rigo/ui/gtk3/widgets/apptreeview.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- """ -Copyright (C) 2009 Canonical Copyright (C) 2012 Fabio Erculiani Authors: - Michael Vogt Fabio Erculiani This program is free software; you can redistribute it and/or modify it under @@ -31,65 +29,48 @@ from cellrenderers import CellRendererAppView, CellButtonRenderer, \ from rigo.em import em, StockEms from rigo.enums import Icons, AppActions from rigo.models.application import Application +from rigo.ui.gtk3.widgets.generictreeview import GenericTreeView from RigoDaemon.enums import AppActions as DaemonAppActions -COL_ROW_DATA = 0 -class AppTreeView(Gtk.TreeView): - - """Treeview based view component that takes a AppStore and displays it""" +class AppTreeView(GenericTreeView): VARIANT_INFO = 0 VARIANT_REMOVE = 1 VARIANT_INSTALL = 2 VARIANT_INSTALLING = 3 VARIANT_REMOVING = 4 + COL_ROW_DATA = 0 def __init__(self, entropy_client, rigo_service, apc, icons, show_ratings, icon_size, store=None): - Gtk.TreeView.__init__(self) self._entropy = entropy_client self._apc = apc self._service = rigo_service - self.pressed = False - self.focal_btn = None - self.expanded_path = None + Gtk.TreeView.__init__(self) - #~ # if this hacked mode is available everything will be fast - #~ # and we can set fixed_height mode and still have growing rows - #~ # (see upstream gnome #607447) - try: - self.set_property("ubuntu-almost-fixed-height-mode", True) - self.set_fixed_height_mode(True) - except Exception: - pass - - self.set_headers_visible(False) - - # a11y: this is a cell renderer that only displays a icon, but still - # has a markup property for orca and friends - # we use it so that orca and other a11y tools get proper text to read - # it needs to be the first one, because that is what the tools look - # at by default tr = CellRendererAppView(icons, self.create_pango_layout(""), show_ratings, Icons.INSTALLED_OVERLAY) tr.set_pixbuf_width(icon_size) - tr.set_button_spacing(em(0.3)) + GenericTreeView.__init__( + self, self._row_activated_callback, + self._button_activated_callback, tr) + # create buttons and set initial strings - info = CellButtonRenderer(self, - name=CellButtonIDs.INFO) + info = CellButtonRenderer( + self, name=CellButtonIDs.INFO) info.set_markup_variants( {self.VARIANT_INFO: _('More Info')}) - action = CellButtonRenderer(self, - name=CellButtonIDs.ACTION) + action = CellButtonRenderer( + self, name=CellButtonIDs.ACTION) action.set_markup_variants( {self.VARIANT_INSTALL: _('Install'), self.VARIANT_REMOVE: _('Remove'), @@ -99,26 +80,16 @@ class AppTreeView(Gtk.TreeView): tr.button_pack_start(info) tr.button_pack_end(action) - column = Gtk.TreeViewColumn("Applications", tr, - application=COL_ROW_DATA) + column = Gtk.TreeViewColumn( + "Applications", tr, + application=self.COL_ROW_DATA) column.set_cell_data_func(tr, self._cell_data_func_cb) column.set_fixed_width(200) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self.append_column(column) - # custom cursor - self._cursor_hand = Gdk.Cursor.new(Gdk.CursorType.HAND2) - - self.connect("style-updated", self._on_style_updated, tr) - # button and motion are "special" - self.connect("button-press-event", self._on_button_press_event, tr) - self.connect("button-release-event", self._on_button_release_event, tr) self.connect("key-press-event", self._on_key_press_event, tr) self.connect("key-release-event", self._on_key_release_event, tr) - self.connect("motion-notify-event", self._on_motion, tr) - self.connect("cursor-changed", self._on_cursor_changed, tr) - # our own "activate" handler - self.connect("row-activated", self._on_row_activated, tr) self._service.connect( "application-processed", @@ -134,6 +105,9 @@ class AppTreeView(Gtk.TreeView): self.set_search_equal_func(self._app_search, None) self.set_property("enable-search", True) + def _row_activated_callback(self, path, rowref): + self._apc.emit("application-activated", + self.model.get_application(rowref)) def _app_search(self, model, column, key, iterator, data): pkg_match = model.get_value(iterator, 0) @@ -142,122 +116,6 @@ class AppTreeView(Gtk.TreeView): app = Application(self._entropy, None, self._service, pkg_match) return not app.search(key) - @property - def appmodel(self): - model = self.get_model() - if isinstance(model, Gtk.TreeModelFilter): - return model.get_model() - return model - - def clear_model(self): - vadjustment = self.get_scrolled_window_vadjustment() - if vadjustment: - vadjustment.set_value(0) - self.expanded_path = None - if self.appmodel: - self.appmodel.clear() - - def expand_path(self, path): - if path is not None and not isinstance(path, Gtk.TreePath): - raise TypeError("Expects Gtk.TreePath or None, got %s" % type(path)) - - model = self.get_model() - old = self.expanded_path - self.expanded_path = path - - if old is not None: - try: - # lazy solution to Bug #846204 - model.row_changed(old, model.get_iter(old)) - except Exception: - pass - if path == None: return - - model.row_changed(path, model.get_iter(path)) - return - - def get_scrolled_window_vadjustment(self): - ancestor = self.get_ancestor(Gtk.ScrolledWindow) - if ancestor: - return ancestor.get_vadjustment() - return None - - def get_rowref(self, model, path): - if path == None: return None - return model[path][COL_ROW_DATA] - - def _calc_row_heights(self, tr): - ypad = StockEms.SMALL - tr.set_property('xpad', StockEms.MEDIUM) - tr.set_property('ypad', ypad) - - for btn in tr.get_buttons(): - # recalc button geometry and cache - btn.configure_geometry(self.create_pango_layout("")) - - btn_h = btn.height - - tr.normal_height = max(32 + 4*ypad, em(2.5) + 4*ypad) - tr.selected_height = tr.normal_height + btn_h + StockEms.MEDIUM \ - + ypad - return - - def _on_style_updated(self, widget, tr): - self._calc_row_heights(tr) - return - - def _on_motion(self, tree, event, tr): - window = self.get_window() - x, y = int(event.x), int(event.y) - if not self._xy_is_over_focal_row(x, y): - window.set_cursor(None) - return - - path = tree.get_path_at_pos(x, y) - if not path: - window.set_cursor(None) - return - - rowref = self.get_rowref(tree.get_model(), path[0]) - if not rowref: - return - - use_hand = False - for btn in tr.get_buttons(): - if btn.state == Gtk.StateFlags.INSENSITIVE: - continue - - if btn.point_in(x, y): - use_hand = True - if self.focal_btn is btn: - btn.set_state(Gtk.StateFlags.ACTIVE) - elif not self.pressed: - btn.set_state(Gtk.StateFlags.PRELIGHT) - else: - if btn.state != Gtk.StateFlags.NORMAL: - btn.set_state(Gtk.StateFlags.NORMAL) - - if use_hand: - window.set_cursor(self._cursor_hand) - else: - window.set_cursor(None) - return - - def _on_cursor_changed(self, view, tr): - model = view.get_model() - sel = view.get_selection() - path = view.get_cursor()[0] - - rowref = self.get_rowref(model, path) - if not rowref: return - - if self.has_focus(): - self.grab_focus() - - sel.select_path(path) - self._update_selected_row(view, tr, path) - return - def _update_selected_row(self, view, tr, path=None): sel = view.get_selection() if not sel: @@ -269,13 +127,13 @@ class AppTreeView(Gtk.TreeView): # update active app, use row-ref as argument self.expand_path(row) - pkg_match = model[row][COL_ROW_DATA] + pkg_match = model[row][self.COL_ROW_DATA] action_btn = tr.get_button_by_name( CellButtonIDs.ACTION) #if not action_btn: return False - app = self.appmodel.get_application(pkg_match) + app = self.model.get_application(pkg_match) app_action = self._get_app_transaction(app) if app_action is None: if app.is_installed(): @@ -312,74 +170,6 @@ class AppTreeView(Gtk.TreeView): self._apc.emit("application-selected", app) return False - def _on_row_activated(self, view, path, column, tr): - rowref = self.get_rowref(view.get_model(), path) - - if not rowref: - return - - x, y = self.get_pointer() - for btn in tr.get_buttons(): - if btn.point_in(x, y): - return - - self._apc.emit("application-activated", - self.appmodel.get_application(rowref)) - return - - def _on_button_event_get_path(self, view, event): - if event.button != 1: - return False - - res = view.get_path_at_pos(int(event.x), int(event.y)) - if not res: - return False - - # check the path is valid and is not a category row - path = res[0] - if path is None: - return False - - # only act when the selection is already there - selection = view.get_selection() - if not selection.path_is_selected(path): - return False - - return path - - def _on_button_press_event(self, view, event, tr): - if not self._on_button_event_get_path(view, event): - return - - self.pressed = True - x, y = int(event.x), int(event.y) - for btn in tr.get_buttons(): - if btn.point_in(x, y) and (btn.state != Gtk.StateFlags.INSENSITIVE): - self.focal_btn = btn - btn.set_state(Gtk.StateFlags.ACTIVE) - view.queue_draw() - return - self.focal_btn = None - return - - def _on_button_release_event(self, view, event, tr): - path = self._on_button_event_get_path(view, event) - if not path: return - - self.pressed = False - x, y = int(event.x), int(event.y) - for btn in tr.get_buttons(): - if btn.point_in(x, y) and (btn.state != Gtk.StateFlags.INSENSITIVE): - btn.set_state(Gtk.StateFlags.NORMAL) - self.get_window().set_cursor(self._cursor_hand) - if self.focal_btn is not btn: - break - self._init_activated(btn, view.get_model(), path) - view.queue_draw() - break - self.focal_btn = None - return - def _on_key_press_event(self, widget, event, tr): kv = event.keyval #print kv @@ -408,7 +198,6 @@ class AppTreeView(Gtk.TreeView): model, it = sel.get_selected() path = model.get_path(it) if path: - #self._init_activated(btn, self.get_model(), path) r = True break @@ -434,36 +223,12 @@ class AppTreeView(Gtk.TreeView): self.queue_draw() return r - def _init_activated(self, btn, model, path): - app = model[path][COL_ROW_DATA] - s = Gtk.Settings.get_default() - GObject.timeout_add(s.get_property("gtk-timeout-initial"), - self._app_activated_cb, - btn, - btn.name, - app, - model, - path) - return - - def _cell_data_func_cb(self, col, cell, model, it, user_data): - - path = model.get_path(it) - - if model[path][0] is None: - indices = path.get_indices() - model.load_range(indices, 5) - - is_active = path == self.expanded_path - cell.set_property('isactive', is_active) - return - - def _app_activated_cb(self, btn, btn_id, pkg_match, store, path): + def _button_activated_callback(self, btn, btn_id, pkg_match, store, path): if isinstance(store, Gtk.TreeModelFilter): store = store.get_model() - app = self.appmodel.get_application(pkg_match) + app = self.model.get_application(pkg_match) if btn_id == CellButtonIDs.INFO: self._apc.emit("application-activated", app) elif btn_id == CellButtonIDs.ACTION: @@ -490,14 +255,6 @@ class AppTreeView(Gtk.TreeView): self.queue_draw() return False - def _set_cursor(self, btn, cursor): - # make sure we have a window instance (LP: #617004) - window = self.get_window() - if isinstance(window, Gdk.Window): - x, y = self.get_pointer() - if btn.point_in(x, y): - window.set_cursor(cursor) - def _on_transaction_started(self, widget, app, daemon_action, tr): """ callback when an application install/remove @@ -601,10 +358,3 @@ class AppTreeView(Gtk.TreeView): local_txs = self._service.local_transactions() action = local_txs.pop(pkg_match, None) return action - - def _xy_is_over_focal_row(self, x, y): - res = self.get_path_at_pos(x, y) - #cur = self.get_cursor() - if not res: - return False - return self.get_path_at_pos(x, y)[0] == self.get_cursor()[0] diff --git a/rigo/rigo/ui/gtk3/widgets/cellrenderers.py b/rigo/rigo/ui/gtk3/widgets/cellrenderers.py index 1b2f13f8d..89f402696 100644 --- a/rigo/rigo/ui/gtk3/widgets/cellrenderers.py +++ b/rigo/rigo/ui/gtk3/widgets/cellrenderers.py @@ -69,6 +69,7 @@ class CellRendererAppView(Gtk.CellRendererText): self.pixbuf_width = 0 self.apptitle_width = 0 self.apptitle_height = 0 + self.markup_height = 0 self.normal_height = 0 self.selected_height = 0 self.show_ratings = show_ratings @@ -341,7 +342,7 @@ class CellRendererAppView(Gtk.CellRendererText): if not pkg_match: return - self.model = widget.appmodel + self.model = widget.model app = self.model.get_application(pkg_match) context = widget.get_style_context() @@ -432,6 +433,7 @@ class CellRendererConfigUpdateView(Gtk.CellRendererText): self.pixbuf_width = 0 self.title_width = 0 self.title_height = 0 + self.markup_height = 0 self.normal_height = 0 self.selected_height = 0 @@ -582,7 +584,7 @@ class CellRendererConfigUpdateView(Gtk.CellRendererText): if not cu: return - self.model = widget.confmodel + self.model = widget.model context = widget.get_style_context() xpad = self.get_property('xpad') ypad = self.get_property('ypad') @@ -613,6 +615,228 @@ class CellRendererConfigUpdateView(Gtk.CellRendererText): is_rtl) context.restore() + + +class CellRendererNoticeView(Gtk.CellRendererText): + + _ICON = None + _ICON_MUTEX = Lock() + + __gproperties__ = { + 'notice' : (GObject.TYPE_PYOBJECT, 'document', + 'a Notice object', + GObject.PARAM_READWRITE), + + 'isactive' : (bool,'isactive', 'is cell active/selected', False, + GObject.PARAM_READWRITE), + } + + def __init__(self, icons, icon_size, layout): + GObject.GObject.__init__(self) + + # Icons + self._icons = icons + self._icon_size = icon_size + + # geometry-state values + self.pixbuf_width = 0 + self.title_width = 0 + self.title_height = 0 + self.markup_height = 0 + self.normal_height = 0 + self.selected_height = 0 + + # button packing + self.button_spacing = 0 + self._buttons = {Gtk.PackType.START: [], + Gtk.PackType.END: []} + self._all_buttons = {} + + # cache a layout + self._layout = layout + + @property + def _icon(self): + if CellRendererNoticeView._ICON is not None: + return CellRendererNoticeView._ICON + with CellRendererNoticeView._ICON_MUTEX: + if CellRendererNoticeView._ICON is not None: + return CellRendererNoticeView._ICON + _icon = self._icons.load_icon( + Icons.CONFIGURATION_FILE, + self._icon_size, 0) + CellRendererNoticeView._ICON = _icon + return _icon + + def _layout_get_pixel_width(self, layout): + return layout.get_size()[0] / Pango.SCALE + + def _layout_get_pixel_height(self, layout): + return layout.get_size()[1] / Pango.SCALE + + def _render_icon(self, cr, cu, cell_area, xpad, ypad, is_rtl): + + icon = self._icon + xo = (self.pixbuf_width - icon.get_width())/2 + + if not is_rtl: + x = cell_area.x + xo + xpad + else: + x = cell_area.x + cell_area.width + xo - \ + self.pixbuf_width - xpad + y = cell_area.y + ypad + + Gdk.cairo_set_source_pixbuf(cr, icon, x, y) + cr.paint() + + def _calculate_height(self, markup): + l = Gtk.Label() + l.set_markup(markup) + w, h = l.get_layout().get_size() + return h / Pango.SCALE + + def _render_summary(self, context, cr, cu, + cell_area, layout, xpad, ypad, + is_rtl): + + markup = cu.get_markup() + self.markup_height = self._calculate_height(markup) + + + layout.set_markup(markup, -1) + + # work out max allowable layout width + layout.set_width(-1) + lw = self._layout_get_pixel_width(layout) + max_layout_width = (cell_area.width - self.pixbuf_width - + 3 * xpad) + max_layout_width = cell_area.width - self.pixbuf_width - 3 * xpad + + if lw >= max_layout_width: + layout.set_width((max_layout_width)*Pango.SCALE) + layout.set_ellipsize(Pango.EllipsizeMode.MIDDLE) + lw = max_layout_width + + self.title_width = cell_area.width - self.pixbuf_width - \ + 10 * xpad + self.title_height = Ems.EM + + if not is_rtl: + x = cell_area.x+2*xpad+self.pixbuf_width + else: + x = cell_area.x+cell_area.width-lw-self.pixbuf_width-2*xpad + + y = cell_area.y + ypad + + Gtk.render_layout(context, cr, x, y, layout) + + def _render_buttons( + self, context, cr, cell_area, layout, xpad, ypad, is_rtl): + + # layout buttons and paint + y = cell_area.y + cell_area.height - ypad + spacing = self.button_spacing + + if not is_rtl: + start = Gtk.PackType.START + end = Gtk.PackType.END + xs = cell_area.x + 2*xpad + self.pixbuf_width + xb = cell_area.x + cell_area.width - xpad + else: + start = Gtk.PackType.END + end = Gtk.PackType.START + xs = cell_area.x + xpad + xb = cell_area.x + cell_area.width - 2*xpad - \ + self.pixbuf_width + + for btn in self._buttons[start]: + btn.set_position(xs, y-btn.height) + btn.render(context, cr, layout) + xs += btn.width + spacing + + for btn in self._buttons[end]: + xb -= btn.width + btn.set_position(xb, y-btn.height) + btn.render(context, cr, layout) + + xb -= spacing + + def set_pixbuf_width(self, w): + self.pixbuf_width = w + + def set_button_spacing(self, spacing): + self.button_spacing = spacing + + def get_button_by_name(self, name): + if name in self._all_buttons: + return self._all_buttons[name] + + def get_buttons(self): + btns = () + for k, v in self._buttons.items(): + btns += tuple(v) + return btns + + def button_pack(self, btn, pack_type=Gtk.PackType.START): + self._buttons[pack_type].append(btn) + self._all_buttons[btn.name] = btn + + def button_pack_start(self, btn): + self.button_pack(btn, Gtk.PackType.START) + + def button_pack_end(self, btn): + self.button_pack(btn, Gtk.PackType.END) + + def do_set_property(self, pspec, value): + setattr(self, pspec.name, value) + + def do_get_property(self, pspec): + return getattr(self, pspec.name) + + def do_get_preferred_height_for_width(self, treeview, width): + if not self.get_properties("isactive")[0]: + return self.normal_height, self.normal_height + return self.selected_height, self.selected_height + + def do_render(self, cr, widget, bg_area, cell_area, flags): + notice = self.props.notice + if not notice: + return + + widget._calc_row_heights(self) + + + self.model = widget.model + context = widget.get_style_context() + xpad = self.get_property('xpad') + ypad = self.get_property('ypad') + is_rtl = widget.get_direction() == Gtk.TextDirection.RTL + + layout = self._layout + context.save() + + self._render_icon(cr, notice, + cell_area, + xpad, ypad, + is_rtl) + + self._render_summary(context, cr, notice, + cell_area, + layout, + xpad, ypad, + is_rtl) + + # below is the stuff that is only done for the active cell + if not self.props.isactive: + return + + self._render_buttons(context, cr, + cell_area, + layout, + xpad, ypad, + is_rtl) + + context.restore() return diff --git a/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py b/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py index 92459bd4c..425eb4eb7 100644 --- a/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py +++ b/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- """ -Copyright (C) 2009 Canonical Copyright (C) 2012 Fabio Erculiani Authors: - Michael Vogt Fabio Erculiani This program is free software; you can redistribute it and/or modify it under @@ -32,8 +30,10 @@ from cellrenderers import CellButtonRenderer, \ from rigo.em import em, StockEms, Ems from rigo.ui.gtk3.models.confupdateliststore import ConfigUpdatesListStore +from rigo.ui.gtk3.widgets.generictreeview import GenericTreeView -class ConfigUpdatesTreeView(Gtk.TreeView): + +class ConfigUpdatesTreeView(GenericTreeView): __gsignals__ = { # Source configuration file edit signal @@ -66,22 +66,20 @@ class ConfigUpdatesTreeView(Gtk.TreeView): VARIANT_DIFF = 2 VARIANT_MERGE = 3 VARIANT_DISCARD = 4 - COL_ROW_DATA = 0 def __init__(self, icons, icon_size): Gtk.TreeView.__init__(self) - self.pressed = False - self.focal_btn = None - self.expanded_path = None - self.set_headers_visible(False) tr = CellRendererConfigUpdateView( icons, ConfigUpdatesListStore.ICON_SIZE, self.create_pango_layout("")) tr.set_pixbuf_width(icon_size) - tr.set_button_spacing(em(0.3)) + GenericTreeView.__init__(self, + self._row_activated_callback, + self._button_activated_callback, tr) + # create buttons and set initial strings edit_source = CellButtonRenderer( self, name=ConfigUpdateCellButtonIDs.EDIT) @@ -111,226 +109,14 @@ class ConfigUpdatesTreeView(Gtk.TreeView): confupdate=self.COL_ROW_DATA) column.set_cell_data_func(tr, self._cell_data_func_cb) - column.set_fixed_width(200) + column.set_fixed_width(350) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self.append_column(column) - # custom cursor - self._cursor_hand = Gdk.Cursor.new(Gdk.CursorType.HAND2) - - self.connect("style-updated", self._on_style_updated, tr) - # button and motion are "special" - self.connect("button-press-event", self._on_button_press_event, tr) - self.connect("button-release-event", self._on_button_release_event, tr) - self.connect("motion-notify-event", self._on_motion, tr) - self.connect("cursor-changed", self._on_cursor_changed, tr) - # our own "activate" handler - self.connect("row-activated", self._on_row_activated, tr) - - @property - def confmodel(self): - model = self.get_model() - if isinstance(model, Gtk.TreeModelFilter): - return model.get_model() - return model - - def clear_model(self): - vadjustment = self.get_scrolled_window_vadjustment() - if vadjustment: - vadjustment.set_value(0) - self.expanded_path = None - confmodel = self.confmodel - if confmodel: - confmodel.clear() - - def expand_path(self, path): - if path is not None and not isinstance(path, Gtk.TreePath): - raise TypeError( - "Expects Gtk.TreePath or None, got %s" % type(path)) - - model = self.get_model() - old = self.expanded_path - self.expanded_path = path - - if old is not None: - try: - # lazy solution to Bug #846204 - model.row_changed(old, model.get_iter(old)) - except Exception: - pass - - if path is None: - return - model.row_changed(path, model.get_iter(path)) - - def get_scrolled_window_vadjustment(self): - ancestor = self.get_ancestor(Gtk.ScrolledWindow) - if ancestor: - return ancestor.get_vadjustment() - - def get_rowref(self, model, path): - if path is None: - return None - return model[path][self.COL_ROW_DATA] - - def _calc_row_heights(self, tr): - ypad = StockEms.SMALL - tr.set_property('xpad', StockEms.MEDIUM) - tr.set_property('ypad', ypad) - - for btn in tr.get_buttons(): - # recalc button geometry and cache - btn.configure_geometry(self.create_pango_layout("")) - - btn_h = btn.height - - tr.normal_height = max(32 + 4*ypad, em(2.5) + 4*ypad) - tr.selected_height = tr.normal_height + btn_h + StockEms.MEDIUM \ - + ypad - - def _on_style_updated(self, widget, tr): - self._calc_row_heights(tr) - - def _on_motion(self, tree, event, tr): - window = self.get_window() - x, y = int(event.x), int(event.y) - if not self._xy_is_over_focal_row(x, y): - window.set_cursor(None) - return - - path = tree.get_path_at_pos(x, y) - if not path: - window.set_cursor(None) - return - - rowref = self.get_rowref(tree.get_model(), path[0]) - if not rowref: - return - - use_hand = False - for btn in tr.get_buttons(): - if btn.state == Gtk.StateFlags.INSENSITIVE: - continue - - if btn.point_in(x, y): - use_hand = True - if self.focal_btn is btn: - btn.set_state(Gtk.StateFlags.ACTIVE) - elif not self.pressed: - btn.set_state(Gtk.StateFlags.PRELIGHT) - else: - if btn.state != Gtk.StateFlags.NORMAL: - btn.set_state(Gtk.StateFlags.NORMAL) - - if use_hand: - window.set_cursor(self._cursor_hand) - else: - window.set_cursor(None) - - def _on_cursor_changed(self, view, tr): - model = view.get_model() - sel = view.get_selection() - path = view.get_cursor()[0] - - rowref = self.get_rowref(model, path) - if not rowref: - return - - if self.has_focus(): - self.grab_focus() - - sel.select_path(path) - self._update_selected_row(view, tr, path) - - def _update_selected_row(self, view, tr, path=None): - sel = view.get_selection() - if not sel: - return False - model, rows = sel.get_selected_rows() - if not rows: - return False - row = rows[0] - - # update active app, use row-ref as argument - self.expand_path(row) - return False - - def _on_row_activated(self, view, path, column, tr): - rowref = self.get_rowref(view.get_model(), path) - - if not rowref: - return - - x, y = self.get_pointer() - for btn in tr.get_buttons(): - if btn.point_in(x, y): - return + def _row_activated_callback(self, path, rowref): self.emit("source-edit", path, rowref) - def _on_button_event_get_path(self, view, event): - if event.button != 1: - return False - - res = view.get_path_at_pos(int(event.x), int(event.y)) - if not res: - return False - - path = res[0] - # only act when the selection is already there - selection = view.get_selection() - if not selection.path_is_selected(path): - return False - - return path - - def _on_button_press_event(self, view, event, tr): - if not self._on_button_event_get_path(view, event): - return - - self.pressed = True - x, y = int(event.x), int(event.y) - for btn in tr.get_buttons(): - if btn.point_in(x, y) and \ - (btn.state != Gtk.StateFlags.INSENSITIVE): - self.focal_btn = btn - btn.set_state(Gtk.StateFlags.ACTIVE) - view.queue_draw() - return - self.focal_btn = None - - def _on_button_release_event(self, view, event, tr): - path = self._on_button_event_get_path(view, event) - if not path: - return - - self.pressed = False - x, y = int(event.x), int(event.y) - for btn in tr.get_buttons(): - if btn.point_in(x, y) and \ - (btn.state != Gtk.StateFlags.INSENSITIVE): - btn.set_state(Gtk.StateFlags.NORMAL) - self.get_window().set_cursor(self._cursor_hand) - if self.focal_btn is not btn: - break - self._init_activated(btn, view.get_model(), path) - view.queue_draw() - break - self.focal_btn = None - - def _init_activated(self, btn, model, path): - cu = model[path][self.COL_ROW_DATA] - s = Gtk.Settings.get_default() - GObject.timeout_add( - s.get_property("gtk-timeout-initial"), - self._confupdate_activated_cb, - btn, btn.name, cu, model, path) - - def _cell_data_func_cb(self, col, cell, model, it, user_data): - path = model.get_path(it) - is_active = path == self.expanded_path - cell.set_property('isactive', is_active) - - def _confupdate_activated_cb(self, btn, btn_id, cu, store, path): + def _button_activated_callback(self, btn, btn_id, cu, store, path): if isinstance(store, Gtk.TreeModelFilter): store = store.get_model() if btn_id == ConfigUpdateCellButtonIDs.EDIT: @@ -344,16 +130,3 @@ class ConfigUpdatesTreeView(Gtk.TreeView): self.emit("source-discard", path, cu) self.expanded_path = None return False - - def _set_cursor(self, btn, cursor): - window = self.get_window() - if isinstance(window, Gdk.Window): - x, y = self.get_pointer() - if btn.point_in(x, y): - window.set_cursor(cursor) - - def _xy_is_over_focal_row(self, x, y): - res = self.get_path_at_pos(x, y) - if not res: - return False - return self.get_path_at_pos(x, y)[0] == self.get_cursor()[0] diff --git a/rigo/rigo/ui/gtk3/widgets/generictreeview.py b/rigo/rigo/ui/gtk3/widgets/generictreeview.py new file mode 100644 index 000000000..b33caa87b --- /dev/null +++ b/rigo/rigo/ui/gtk3/widgets/generictreeview.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2009 Canonical +Copyright (C) 2012 Fabio Erculiani + +Authors: + Michael Vogt + Fabio Erculiani + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +from gi.repository import Gtk, Gdk, GObject + +from rigo.em import em, StockEms, Ems + + +class GenericTreeView(Gtk.TreeView): + + COL_ROW_DATA = 0 + + def __init__(self, row_activated_callback, button_activated_callback, tr): + self._row_activated_callback = row_activated_callback + self._button_activated_callback = button_activated_callback + Gtk.TreeView.__init__(self) + self.pressed = False + self.focal_btn = None + self.expanded_path = None + self.set_headers_visible(False) + + # custom cursor + self._cursor_hand = Gdk.Cursor.new(Gdk.CursorType.HAND2) + + self.connect("style-updated", self._on_style_updated, tr) + # button and motion are "special" + self.connect("button-press-event", self._on_button_press_event, tr) + self.connect("button-release-event", self._on_button_release_event, tr) + self.connect("motion-notify-event", self._on_motion, tr) + self.connect("cursor-changed", self._on_cursor_changed, tr) + # our own "activate" handler + self.connect("row-activated", self._on_row_activated, tr) + + @property + def model(self): + model = self.get_model() + if isinstance(model, Gtk.TreeModelFilter): + return model.get_model() + return model + + def clear_model(self): + vadjustment = self.get_scrolled_window_vadjustment() + if vadjustment: + vadjustment.set_value(0) + self.expanded_path = None + model = self.model + if model: + model.clear() + + def expand_path(self, path): + if path is not None and not isinstance(path, Gtk.TreePath): + raise TypeError( + "Expects Gtk.TreePath or None, got %s" % type(path)) + + model = self.get_model() + old = self.expanded_path + self.expanded_path = path + + if old is not None: + try: + # lazy solution to Bug #846204 + model.row_changed(old, model.get_iter(old)) + except Exception: + pass + + if path is None: + return + model.row_changed(path, model.get_iter(path)) + + def get_scrolled_window_vadjustment(self): + ancestor = self.get_ancestor(Gtk.ScrolledWindow) + if ancestor: + return ancestor.get_vadjustment() + + def get_rowref(self, model, path): + if path is None: + return None + return model[path][self.COL_ROW_DATA] + + def _calc_row_heights(self, tr): + ypad = StockEms.SMALL + tr.set_property('xpad', StockEms.MEDIUM) + tr.set_property('ypad', ypad) + + btn = None + for btn in tr.get_buttons(): + # recalc button geometry and cache + btn.configure_geometry(self.create_pango_layout("")) + + if btn is None: + btn_h = 0 + else: + btn_h = btn.height + + normal_height = max(32 + 4*ypad, em(2.5) + 4*ypad) + markup_height = tr.markup_height + if markup_height > 0: + markup_height -= normal_height + tr.normal_height = normal_height + markup_height + + tr.selected_height = tr.normal_height + btn_h + StockEms.MEDIUM \ + + ypad + + def _on_style_updated(self, widget, tr): + self._calc_row_heights(tr) + + def _on_motion(self, tree, event, tr): + window = self.get_window() + x, y = int(event.x), int(event.y) + if not self._xy_is_over_focal_row(x, y): + window.set_cursor(None) + return + + path = tree.get_path_at_pos(x, y) + if not path: + window.set_cursor(None) + return + + rowref = self.get_rowref(tree.get_model(), path[0]) + if not rowref: + return + + use_hand = False + for btn in tr.get_buttons(): + if btn.state == Gtk.StateFlags.INSENSITIVE: + continue + + if btn.point_in(x, y): + use_hand = True + if self.focal_btn is btn: + btn.set_state(Gtk.StateFlags.ACTIVE) + elif not self.pressed: + btn.set_state(Gtk.StateFlags.PRELIGHT) + else: + if btn.state != Gtk.StateFlags.NORMAL: + btn.set_state(Gtk.StateFlags.NORMAL) + + if use_hand: + window.set_cursor(self._cursor_hand) + else: + window.set_cursor(None) + + def _on_cursor_changed(self, view, tr): + model = view.get_model() + sel = view.get_selection() + path = view.get_cursor()[0] + + rowref = self.get_rowref(model, path) + if not rowref: + return + + if self.has_focus(): + self.grab_focus() + + sel.select_path(path) + self._update_selected_row(view, tr, path) + + def _update_selected_row(self, view, tr, path=None): + sel = view.get_selection() + if not sel: + return False + model, rows = sel.get_selected_rows() + if not rows: + return False + row = rows[0] + + # update active app, use row-ref as argument + self.expand_path(row) + return False + + def _on_row_activated(self, view, path, column, tr): + rowref = self.get_rowref(view.get_model(), path) + + if not rowref: + return + + x, y = self.get_pointer() + for btn in tr.get_buttons(): + if btn.point_in(x, y): + return + self._row_activated_callback(path, rowref) + + def _on_button_event_get_path(self, view, event): + if event.button != 1: + return False + + res = view.get_path_at_pos(int(event.x), int(event.y)) + if not res: + return False + + path = res[0] + # only act when the selection is already there + selection = view.get_selection() + if not selection.path_is_selected(path): + return False + + return path + + def _on_button_press_event(self, view, event, tr): + if not self._on_button_event_get_path(view, event): + return + + self.pressed = True + x, y = int(event.x), int(event.y) + for btn in tr.get_buttons(): + if btn.point_in(x, y) and \ + (btn.state != Gtk.StateFlags.INSENSITIVE): + self.focal_btn = btn + btn.set_state(Gtk.StateFlags.ACTIVE) + view.queue_draw() + return + self.focal_btn = None + + def _on_button_release_event(self, view, event, tr): + path = self._on_button_event_get_path(view, event) + if not path: + return + + self.pressed = False + x, y = int(event.x), int(event.y) + for btn in tr.get_buttons(): + if btn.point_in(x, y) and \ + (btn.state != Gtk.StateFlags.INSENSITIVE): + btn.set_state(Gtk.StateFlags.NORMAL) + self.get_window().set_cursor(self._cursor_hand) + if self.focal_btn is not btn: + break + self._init_activated(btn, view.get_model(), path) + view.queue_draw() + break + self.focal_btn = None + + def _init_activated(self, btn, model, path): + obj = model[path][self.COL_ROW_DATA] + s = Gtk.Settings.get_default() + GObject.timeout_add( + s.get_property("gtk-timeout-initial"), + self._activated_callback, + btn, btn.name, obj, model, path) + + def _cell_data_func_cb(self, col, cell, model, it, user_data): + path = model.get_path(it) + is_active = path == self.expanded_path + cell.set_property('isactive', is_active) + + def _activated_callback(self, btn, btn_id, obj, store, path): + if self._button_activated_callback is not None: + return self._button_activated_callback( + btn, btn_id, obj, store, path) + return False + + def _set_cursor(self, btn, cursor): + window = self.get_window() + if isinstance(window, Gdk.Window): + x, y = self.get_pointer() + if btn.point_in(x, y): + window.set_cursor(cursor) + + def _xy_is_over_focal_row(self, x, y): + res = self.get_path_at_pos(x, y) + if not res: + return False + return self.get_path_at_pos(x, y)[0] == self.get_cursor()[0] diff --git a/rigo/rigo/ui/gtk3/widgets/noticeboardtreeview.py b/rigo/rigo/ui/gtk3/widgets/noticeboardtreeview.py new file mode 100644 index 000000000..e5058e89c --- /dev/null +++ b/rigo/rigo/ui/gtk3/widgets/noticeboardtreeview.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2012 Fabio Erculiani + +Authors: + Fabio Erculiani + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +from gi.repository import Gtk, Gdk, GObject, Pango +import os +from threading import Lock + +from entropy.i18n import _ + +from cellrenderers import CellButtonRenderer, CellRendererNoticeView + +from rigo.em import em, StockEms, Ems +from rigo.utils import open_url +from rigo.ui.gtk3.models.noticeboardliststore import NoticeBoardListStore + +from rigo.ui.gtk3.widgets.generictreeview import GenericTreeView + + +class NoticeBoardTreeView(GenericTreeView): + + def __init__(self, icons, icon_size): + Gtk.TreeView.__init__(self) + + tr = CellRendererNoticeView( + icons, NoticeBoardListStore.ICON_SIZE, + self.create_pango_layout("")) + tr.set_pixbuf_width(icon_size) + + GenericTreeView.__init__( + self, self._row_activated_callback, None, tr) + + column = Gtk.TreeViewColumn("Notices", tr, + notice=self.COL_ROW_DATA) + + column.set_cell_data_func(tr, self._cell_data_func_cb) + column.set_fixed_width(350) + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + self.append_column(column) + + def _row_activated_callback(self, path, rowref): + open_url(rowref.link()) diff --git a/rigo/rigo/ui/gtk3/widgets/notifications.py b/rigo/rigo/ui/gtk3/widgets/notifications.py index 7e82b2cf1..2cc562be1 100644 --- a/rigo/rigo/ui/gtk3/widgets/notifications.py +++ b/rigo/rigo/ui/gtk3/widgets/notifications.py @@ -1012,3 +1012,58 @@ class ConfigUpdatesNotificationBox(NotificationBox): This NotificationBox cannot be destroyed easily. """ return True + + +class NoticeBoardNotificationBox(NotificationBox): + + __gsignals__ = { + "let-me-see" : (GObject.SignalFlags.RUN_LAST, + None, + tuple(), + ), + "stop-annoying" : (GObject.SignalFlags.RUN_LAST, + None, + tuple(), + ), + } + + def __init__(self, avc, notices_len): + + msg = ngettext("There is %d notice from a repository", + "There are %d notices from repositories", + notices_len) + msg = msg % (notices_len,) + + msg += ".\n\n" + msg += _("It is extremely important to" + " always read them.") + msg += "" + + context_id = "NoticeBoardNotificationContextId" + NotificationBox.__init__( + self, prepare_markup(msg), + message_type=Gtk.MessageType.INFO, + context_id=context_id) + + self.add_button(_("Let me see"), self._on_let_me_see) + self.add_button(_("Stop annoying me"), self._on_stop_annoying) + self.add_destroy_button(_("Close")) + + def _on_stop_annoying(self, widget): + """ + Stop showing this notification box as long as there are no + upstream updates. + """ + self.emit("stop-annoying") + + def _on_let_me_see(self, widget): + """ + Show the proposed configuration file updates + """ + self.emit("let-me-see") + + def is_managed(self): + """ + This NotificationBox cannot be destroyed easily. + """ + return True diff --git a/rigo/rigo_app.py b/rigo/rigo_app.py index 39f901668..859166628 100644 --- a/rigo/rigo_app.py +++ b/rigo/rigo_app.py @@ -43,6 +43,7 @@ from rigo.enums import RigoViewStates, LocalActivityStates from rigo.entropyapi import EntropyWebService, EntropyClient as Client from rigo.ui.gtk3.widgets.apptreeview import AppTreeView from rigo.ui.gtk3.widgets.confupdatetreeview import ConfigUpdatesTreeView +from rigo.ui.gtk3.widgets.noticeboardtreeview import NoticeBoardTreeView from rigo.ui.gtk3.widgets.notifications import NotificationBox from rigo.ui.gtk3.controllers.applications import \ ApplicationsViewController @@ -50,6 +51,8 @@ from rigo.ui.gtk3.controllers.application import \ ApplicationViewController from rigo.ui.gtk3.controllers.confupdate import \ ConfigUpdatesViewController +from rigo.ui.gtk3.controllers.noticeboard import \ + NoticeBoardViewController from rigo.ui.gtk3.controllers.notifications import \ UpperNotificationViewController, BottomNotificationViewController @@ -58,6 +61,7 @@ from rigo.ui.gtk3.controllers.work import \ from rigo.ui.gtk3.widgets.welcome import WelcomeBox from rigo.ui.gtk3.models.appliststore import AppListStore from rigo.ui.gtk3.models.confupdateliststore import ConfigUpdatesListStore +from rigo.ui.gtk3.models.noticeboardliststore import NoticeBoardListStore from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme from rigo.utils import escape_markup @@ -117,7 +121,10 @@ class Rigo(Gtk.Application): self._exit_work_state), RigoViewStates.CONFUPDATES_VIEW_STATE: ( self._enter_confupdates_state, - self._exit_confupdates_state,) + self._exit_confupdates_state), + RigoViewStates.NOTICEBOARD_VIEW_STATE: ( + self._enter_noticeboard_state, + self._exit_noticeboard_state) } self._state_mutex = Lock() @@ -150,6 +157,11 @@ class Rigo(Gtk.Application): self._config_view = self._builder.get_object("configViewVbox") self._config_view.set_name("rigo-view") + self._notice_scrolled_view = self._builder.get_object( + "noticeViewScrolledWindow") + self._notice_view = self._builder.get_object("noticeViewVbox") + self._notice_view.set_name("rigo-view") + self._search_entry = self._builder.get_object("searchEntry") self._search_entry_completion = self._builder.get_object( "searchEntryCompletion") @@ -197,6 +209,19 @@ class Rigo(Gtk.Application): self._service.set_configuration_controller(self._config_view_c) + # NoticeBoard model, view and controller + self._notice_store = NoticeBoardListStore() + self._view_notice = NoticeBoardTreeView( + icons, NoticeBoardListStore.ICON_SIZE) + self._notice_scrolled_view.add(self._view_notice) + def _notice_queue_draw(*args): + self._view_notice.queue_draw() + self._notice_store.connect("redraw-request", _notice_queue_draw) + self._notice_view_c = NoticeBoardViewController( + self._notice_store, self._view_notice) + + self._service.set_noticeboard_controller(self._notice_view_c) + self._welcome_box = WelcomeBox() settings = Gtk.Settings.get_default() @@ -238,6 +263,9 @@ class Rigo(Gtk.Application): self._config_view_c.set_notification_controller(self._nc) self._config_view_c.set_applications_controller(self._avc) + self._notice_view_c.set_notification_controller(self._nc) + self._notice_view_c.set_applications_controller(self._avc) + self._service.set_applications_controller(self._avc) self._service.set_application_controller(self._app_view_c) self._service.set_notification_controller(self._nc) @@ -413,6 +441,20 @@ class Rigo(Gtk.Application): """ self._config_view.show() + def _exit_noticeboard_state(self): + """ + Action triggered when UI exits the NoticeBoard + state (or mode). + """ + self._notice_view.hide() + + def _enter_noticeboard_state(self): + """ + Action triggered when UI enters the NoticeBoard + state (or mode). + """ + self._notice_view.show() + def _exit_static_state(self): """ Action triggered when UI exits the Static Browser @@ -690,6 +732,7 @@ class Rigo(Gtk.Application): self._thread_dumper() self._config_view_c.setup() + self._notice_view_c.setup() self._app_view_c.setup() self._avc.setup() self._nc.setup()