[RigoDaemon/Rigo] implement Notice Board support (woot)
This commit is contained in:
+73
-24
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -58,7 +58,8 @@ class RigoViewStates:
|
||||
APPLICATION_VIEW_STATE,
|
||||
WORK_VIEW_STATE,
|
||||
CONFUPDATES_VIEW_STATE,
|
||||
) = range(5)
|
||||
NOTICEBOARD_VIEW_STATE,
|
||||
) = range(6)
|
||||
|
||||
class LocalActivityStates:
|
||||
(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user