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
+
+
+
+ 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()