[RigoDaemon/Rigo] implement Notice Board support (woot)

This commit is contained in:
Fabio Erculiani
2012-04-10 23:37:22 +02:00
parent 388cac9187
commit 161f5205d8
16 changed files with 1202 additions and 538 deletions
+73 -24
View File
@@ -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):
@@ -67,6 +67,8 @@
<method name="reload_configuration_updates"/>
<method name="noticeboards"/>
<method name="action">
<arg name="package_id" type="i" direction="in"/>
<arg name="repository_id" type="s" direction="in"/>
+27 -1
View File
@@ -162,6 +162,32 @@
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="noticeViewVbox">
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="noticeViewScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="appViewScrollWin">
<property name="can_focus">True</property>
@@ -547,7 +573,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
<property name="position">6</property>
</packing>
</child>
</object>
+45
View File
@@ -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):
"""
+2 -1
View File
@@ -58,7 +58,8 @@ class RigoViewStates:
APPLICATION_VIEW_STATE,
WORK_VIEW_STATE,
CONFUPDATES_VIEW_STATE,
) = range(5)
NOTICEBOARD_VIEW_STATE,
) = range(6)
class LocalActivityStates:
(
+115
View File
@@ -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 = "<b>%s</b>\n<small><b>%s</b>, " + \
"<i>%s</i>\n<u>%s</u>\n\n%s</small>"
msg = msg % (
self.title(), self.repository(),
self.date(), self.link(), self.description())
return prepare_markup(msg)
@@ -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":
@@ -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)
@@ -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)
+22 -272
View File
@@ -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]
+226 -2
View File
@@ -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
+10 -237
View File
@@ -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]
@@ -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]
@@ -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())
@@ -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 <b>%d</b> notice from a repository",
"There are <b>%d</b> notices from repositories",
notices_len)
msg = msg % (notices_len,)
msg += ".\n\n<small>"
msg += _("It is <b>extremely</b> important to"
" always read them.")
msg += "</small>"
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
+44 -1
View File
@@ -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()