diff --git a/rigo/RigoDaemon/app.py b/rigo/RigoDaemon/app.py
index 9a1c594e1..c68eadcf3 100755
--- a/rigo/RigoDaemon/app.py
+++ b/rigo/RigoDaemon/app.py
@@ -16,8 +16,12 @@ import os
os.environ['ETP_GETTEXT_DOMAIN'] = "rigo"
import sys
+import pwd
import time
import signal
+import tempfile
+import shutil
+import subprocess
from threading import Lock, Timer, Semaphore
from collections import deque
@@ -509,6 +513,9 @@ class RigoDaemonService(dbus.service.Object):
self._acquired_exclusive = False
self._acquired_exclusive_mutex = Lock()
+ self._config_updates = None
+ self._config_updates_mutex = Lock()
+
self._action_queue = deque()
self._action_queue_length_mutex = Lock()
self._action_queue_length = 0
@@ -618,6 +625,18 @@ class RigoDaemonService(dbus.service.Object):
'org.freedesktop.DBus').GetConnectionUnixProcessID(
sender)
+ def _get_caller_user(self, sender):
+ """
+ Return the username of the caller (through Dbus).
+ """
+ bus = self._bus.get_object(
+ 'org.freedesktop.DBus',
+ '/org/freedesktop/DBus')
+ return dbus.Interface(
+ bus,
+ 'org.freedesktop.DBus').GetConnectionUnixUser(
+ sender)
+
def _authorize(self, pid, action_id):
"""
Authorize privileged Activity.
@@ -814,6 +833,7 @@ class RigoDaemonService(dbus.service.Object):
self.activity_completed, activity, success)
GLib.idle_add(
self.applications_managed, outcome)
+ self._maybe_signal_configuration_updates()
is_app = True
if isinstance(item, RigoDaemonService.ActionQueueItem):
@@ -1561,6 +1581,62 @@ class RigoDaemonService(dbus.service.Object):
count, total), debug=True)
self._entropy.installed_repository().commit()
+ def _maybe_signal_configuration_updates(self):
+ """
+ Signal Configuration Files Updates if needed.
+ """
+ scandata = self._configuration_updates(_force=True)
+ if scandata:
+ GLib.idle_add(
+ self.configuration_updates_available,
+ self._dbus_prepare_configuration_files(
+ scandata.root(), scandata))
+
+ def _dbus_prepare_configuration_files(self, root, scandata):
+ """
+ Prepare the ConfigurationFiles object for sending through
+ dbus.
+ """
+ updates = [(root, source, x['destination'], \
+ x['package_ids'], x['automerge']) for source, x \
+ in scandata.items()]
+ return updates
+
+ def _configuration_updates(self, _force=False):
+ """
+ Return the latest (or a new one if not initialized yet)
+ ConfigurationFiles object.
+ """
+ self._rwsem.reader_acquire()
+ try:
+ with self._config_updates_mutex:
+ if self._config_updates is None or _force:
+ updates = self._entropy.ConfigurationUpdates()
+ scandata = self._enrich_configuration_updates(
+ updates.get())
+ self._config_updates = scandata
+ else:
+ scandata = self._config_updates
+ finally:
+ self._rwsem.reader_release()
+ return scandata
+
+ def _enrich_configuration_updates(self, scandata):
+ """
+ Enrich ConfigurationFiles object returned by Entropy Client
+ with extended information.
+ """
+ _cache = {}
+ inst_repo = self._entropy.installed_repository()
+ for k, v in scandata.items():
+ dest = v['destination']
+ pkg_ids = _cache.get(dest)
+ if pkg_ids is None:
+ pkg_ids = list(inst_repo.searchBelongs(dest))
+ _cache[dest] = pkg_ids
+ v['package_ids'] = pkg_ids
+ del _cache
+ return scandata
def _close_local_resources(self):
"""
@@ -1853,6 +1929,207 @@ class RigoDaemonService(dbus.service.Object):
task.name = "AcceptLicensesThread"
task.start()
+ @dbus.service.method(BUS_NAME, in_signature='s',
+ out_signature='b', sender_keyword='sender')
+ def merge_configuration(self, source, sender=None):
+ """
+ Move configuration file from source path over to
+ destination, keeping destination path permissions.
+ """
+ write_output("move_configuration called: %s" % (locals(),),
+ debug=True)
+ pid = self._get_caller_pid(sender)
+ authenticated = self._auth.authenticate_sync(
+ pid, PolicyActions.MANAGE_CONFIGURATION)
+ if not authenticated:
+ return False
+ updates = self._configuration_updates()
+ return updates.merge(source)
+
+ @dbus.service.method(BUS_NAME, in_signature='s',
+ out_signature='s', sender_keyword='sender')
+ def diff_configuration(self, source, sender=None):
+ """
+ Generate a diff between destination -> source file paths and
+ return a path containing the output to caller. If diff cannot
+ be run, return empty string.
+ """
+ write_output("diff_configuration called: %s" % (locals(),),
+ debug=True)
+ pid = self._get_caller_pid(sender)
+ authenticated = self._auth.authenticate_sync(
+ pid, PolicyActions.MANAGE_CONFIGURATION)
+ if not authenticated:
+ return ""
+
+ updates = self._configuration_updates()
+ root = update.root()
+ obj = updates.get(source)
+ if obj is None:
+ return ""
+
+ uid = self._get_caller_user(sender)
+ source_path = root + source
+ dest_path = root + obj['destination']
+
+ rc = None
+ tmp_fd, tmp_path = None, None
+ path = ""
+ try:
+ tmp_fd, tmp_path = tempfile.mkstemp(prefix="RigoDaemon")
+ with os.fdopen(tmp_fd, "wb") as tmp_f:
+ rc = subprocess.call(
+ ["/usr/bin/diff", "-Nu", dest_path, source_path],
+ stdout = tmp_f)
+ if rc == os.EX_OK:
+ path = self._prepare_configuration_file(
+ tmp_path, uid)
+
+ except (OSError, IOError,) as err:
+ write_output("cannot diff_configuration: %s" % (
+ repr(err),), debug=True)
+
+ finally:
+ if tmp_fd is not None:
+ try:
+ os.close(tmp_fd)
+ except (OSError, IOError):
+ pass
+ if tmp_path is not None:
+ try:
+ os.remove(tmp_path)
+ except OSError:
+ pass
+
+ return path
+
+ def _view_configuration_file(self, source, sender, dest=False):
+ """
+ View a source or destination configuration file
+ """
+ pid = self._get_caller_pid(sender)
+
+ authenticated = self._auth.authenticate_sync(
+ pid, PolicyActions.MANAGE_CONFIGURATION)
+ if not authenticated:
+ return ""
+
+ updates = self._configuration_updates()
+ root = updates.root()
+ obj = updates.get(source)
+ if obj is None:
+ return ""
+
+ uid = self._get_caller_user(sender)
+ if dest:
+ source_path = root + obj['destination']
+ else:
+ source_path = root + source
+ return self._prepare_configuration_file(source_path, uid)
+
+ def _prepare_configuration_file(self, path, uid):
+ """
+ Prepare the given configuration file copying it to
+ a temporary path and setting proper permissions.
+ """
+ tmp_fd, tmp_path = tempfile.mkstemp(prefix="RigoDaemon")
+ try:
+ with os.fdopen(tmp_fd, "wb") as tmp_f:
+ with open(path, "rb") as path_f:
+ shutil.copyfileobj(path_f, tmp_f)
+ # fixup perms
+ os.chown(tmp_path, uid, -1)
+ path = tmp_path
+ except (OSError, IOError) as err:
+ try:
+ os.close(tmp_fd)
+ except OSError:
+ pass
+ try:
+ os.remove(tmp_path)
+ except OSError:
+ pass
+ write_output("cannot _prepare_configuration_file: %s" % (
+ repr(err),), debug=True)
+ path = ""
+ return path
+
+ @dbus.service.method(BUS_NAME, in_signature='s',
+ out_signature='s', sender_keyword='sender')
+ def view_configuration_source(self, source, sender=None):
+ """
+ Copy configuration source file to a temporary path featuring
+ caller ownership. If file cannot be copied, empty string is
+ returned.
+ """
+ write_output("view_configuration_source called: %s" % (locals(),),
+ debug=True)
+ return self._view_configuration_file(source, sender)
+
+ @dbus.service.method(BUS_NAME, in_signature='s',
+ out_signature='s', sender_keyword='sender')
+ def view_configuration_destination(self, source, sender=None):
+ """
+ Copy configuration destination file to a temporary path featuring
+ caller ownership. If file cannot be copied, empty string is
+ returned.
+ """
+ write_output("view_configuration_destination called: %s" % (
+ locals(),),
+ debug=True)
+ return self._view_configuration_file(source, sender, dest=True)
+
+ @dbus.service.method(BUS_NAME, in_signature='s',
+ out_signature='b', sender_keyword='sender')
+ def discard_configuration(self, source, sender=None):
+ """
+ Remove configuration file from source path.
+ """
+ write_output("discard_configuration called: %s" % (locals(),),
+ debug=True)
+ pid = self._get_caller_pid(sender)
+ authenticated = self._auth.authenticate_sync(
+ pid, PolicyActions.MANAGE_CONFIGURATION)
+ if not authenticated:
+ return False
+ updates = self._configuration_updates()
+ return updates.remove(source)
+
+ @dbus.service.method(BUS_NAME, in_signature='',
+ out_signature='')
+ def configuration_updates(self):
+ """
+ Return the last generated (if any, or create a new one)
+ ConfigurationFiles object.
+ """
+ write_output("configuration_updates called", debug=True)
+ task = ParallelTask(self._maybe_signal_configuration_updates)
+ task.name = "ConfigurationUpdatesSignal"
+ task.daemon = True
+ task.start()
+
+ @dbus.service.method(BUS_NAME, in_signature='',
+ out_signature='')
+ def reload_configuration_updates(self):
+ """
+ Load a new ConfigurationFiles object.
+ """
+ write_output("reload_configuration_updates called", debug=True)
+ def _reload():
+ self._rwsem.reader_acquire()
+ try:
+ updates = self._entropy.ConfigurationUpdates()
+ with self._config_updates_mutex:
+ scandata = updates.get()
+ self._config_updates = scandata
+ finally:
+ self._rwsem.reader_release()
+
+ task = ParallelTask(_reload)
+ task.name = "ReloadConfigurationUpdates"
+ task.daemon = True
+ task.start()
+
@dbus.service.method(BUS_NAME, in_signature='',
out_signature='b')
def exclusive(self):
@@ -1950,6 +2227,17 @@ class RigoDaemonService(dbus.service.Object):
write_output("unsupported_applications() issued, args:"
" %s" % (locals(),), debug=True)
+ @dbus.service.signal(dbus_interface=BUS_NAME,
+ signature='a(sssaib)')
+ def configuration_updates_available(self, updates):
+ """
+ Notify the presence of configuration files that should be updated.
+ The payload is a list of tuples, each one composed by:
+ (root, source, destination, installed_package_ids, auto-mergeable)
+ """
+ write_output("configuration_updates_available() issued, args:"
+ " %s" % (locals(),), debug=True)
+
@dbus.service.signal(dbus_interface=BUS_NAME,
signature='i')
def restarting_system_upgrade(self, updates_amount):
diff --git a/rigo/RigoDaemon/authentication.py b/rigo/RigoDaemon/authentication.py
index 88eae6be0..f2f3161d6 100644
--- a/rigo/RigoDaemon/authentication.py
+++ b/rigo/RigoDaemon/authentication.py
@@ -71,3 +71,30 @@ class AuthenticationController(object):
None, # Gio.Cancellable()
_polkit_auth_callback,
self._mainloop)
+
+ def authenticate_sync(self, pid, action_id):
+ """
+ Authenticate current User asking Administrator
+ passwords.
+ Return True if authenticated, False if not.
+ """
+ authority = Polkit.Authority.get()
+ subject = Polkit.UnixProcess.new(pid)
+ result = authority.check_authorization_sync(
+ subject,
+ action_id,
+ None,
+ Polkit.CheckAuthorizationFlags.ALLOW_USER_INTERACTION,
+ None)
+
+ authenticated = False
+ try:
+ if result.get_is_authorized():
+ authenticated = True
+ elif result.get_is_challenge():
+ authenticated = True
+ except GObject.GError as err:
+ const_debug_write(
+ __name__,
+ "_polkit_auth_callback: error: %s" % (err,))
+ return authenticated
diff --git a/rigo/RigoDaemon/config.py b/rigo/RigoDaemon/config.py
index 069568cdd..66fd1e785 100644
--- a/rigo/RigoDaemon/config.py
+++ b/rigo/RigoDaemon/config.py
@@ -21,3 +21,4 @@ class PolicyActions:
UPDATE_REPOSITORIES = "org.sabayon.RigoDaemon.update"
UPGRADE_SYSTEM = "org.sabayon.RigoDaemon.upgrade"
MANAGE_APPLICATIONS = "org.sabayon.RigoDaemon.manage"
+ MANAGE_CONFIGURATION = "org.sabayon.RigoDaemon.configuration"
diff --git a/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml b/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml
index 2af23980a..a3eeacc5d 100644
--- a/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml
+++ b/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml
@@ -31,6 +31,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rigo/RigoDaemon/polkit/org.sabayon.RigoDaemon.policy b/rigo/RigoDaemon/polkit/org.sabayon.RigoDaemon.policy
index b75828914..fec711150 100644
--- a/rigo/RigoDaemon/polkit/org.sabayon.RigoDaemon.policy
+++ b/rigo/RigoDaemon/polkit/org.sabayon.RigoDaemon.policy
@@ -41,4 +41,17 @@
+
+ Manage Configuration
+
+ Authentication is required to Manage System Configuration
+
+ package-x-generic
+
+ no
+ no
+ auth_admin_keep
+
+
+
diff --git a/rigo/data/ui/gtk3/rigo.ui b/rigo/data/ui/gtk3/rigo.ui
index 67adb9449..ce8737f28 100644
--- a/rigo/data/ui/gtk3/rigo.ui
+++ b/rigo/data/ui/gtk3/rigo.ui
@@ -75,7 +75,6 @@
False
+
+
+
+ True
+ True
+ 4
+
+
True
@@ -522,7 +547,7 @@
True
True
- 4
+ 5
diff --git a/rigo/rigo/controllers/daemon.py b/rigo/rigo/controllers/daemon.py
index fbe0d7c6e..6df67f3d2 100644
--- a/rigo/rigo/controllers/daemon.py
+++ b/rigo/rigo/controllers/daemon.py
@@ -29,6 +29,7 @@ from gi.repository import Gtk, GLib, GObject
from rigo.enums import AppActions, RigoViewStates, \
LocalActivityStates
from rigo.models.application import Application
+from rigo.models.configupdate import ConfigUpdate
from rigo.ui.gtk3.widgets.notifications import NotificationBox, \
PleaseWaitNotificationBox, LicensesNotificationBox, \
OrphanedAppsNotificationBox, InstallNotificationBox, \
@@ -69,7 +70,7 @@ class RigoServiceController(GObject.Object):
context_id = RigoServiceController.NOTIFICATION_CONTEXT_ID
NotificationBox.__init__(
self, message,
- tooltip=_("Good luck!"),
+ tooltip=prepare_markup(_("Good luck!")),
message_type=message_type,
context_id=context_id)
@@ -166,6 +167,7 @@ class RigoServiceController(GObject.Object):
_APPLICATIONS_MANAGED_SIGNAL = "applications_managed"
_UNSUPPORTED_APPLICATIONS_SIGNAL = "unsupported_applications"
_RESTARTING_UPGRADE_SIGNAL = "restarting_system_upgrade"
+ _CONFIGURATION_UPDATES_SIGNAL = "configuration_updates_available"
_SUPPORTED_APIS = [0]
def __init__(self, rigo_app, activity_rwsem,
@@ -174,6 +176,7 @@ class RigoServiceController(GObject.Object):
self._rigo = rigo_app
self._activity_rwsem = activity_rwsem
self._nc = None
+ self._confc = None
self._bottom_nc = None
self._wc = None
self._avc = None
@@ -217,16 +220,22 @@ class RigoServiceController(GObject.Object):
def set_applications_controller(self, avc):
"""
- Bind ApplicationsViewController object to this class.
+ Bind an ApplicationsViewController object to this class.
"""
self._avc = avc
def set_application_controller(self, apc):
"""
- Bind ApplicationViewController object to this class.
+ Bind an ApplicationViewController object to this class.
"""
self._apc = apc
+ def set_configuration_controller(self, confc):
+ """
+ Bind a ConfigUpdatesViewController object to this class.
+ """
+ self._confc = confc
+
def set_terminal(self, terminal):
"""
Bind a TerminalWidget to this object, in order to be used with
@@ -459,6 +468,13 @@ class RigoServiceController(GObject.Object):
self._application_enqueued_signal,
dbus_interface=self.DBUS_INTERFACE)
+ # RigoDaemon tells us that there are configuration
+ # file updates available
+ self.__entropy_bus.connect_to_signal(
+ self._CONFIGURATION_UPDATES_SIGNAL,
+ self._configuration_updates_available_signal,
+ dbus_interface=self.DBUS_INTERFACE)
+
return self.__entropy_bus
### GOBJECT EVENTS
@@ -633,7 +649,7 @@ class RigoServiceController(GObject.Object):
box = NotificationBox(
msg,
- tooltip=_("An error occurred"),
+ tooltip=prepare_markup(_("An error occurred")),
message_type=Gtk.MessageType.ERROR,
context_id="ApplicationOutcomeSignalError")
def _show_me(*args):
@@ -726,6 +742,25 @@ class RigoServiceController(GObject.Object):
context_id=self.SYSTEM_UPGRADE_CONTEXT_ID)
self._nc.append(box, timeout=20)
+ def _configuration_updates_available_signal(self, updates):
+ const_debug_write(
+ __name__,
+ "_configuration_updates_available_signal: "
+ "updates: %s" % (updates,))
+
+ if self._confc is not None:
+ config_updates = []
+ for root, source, dest, pkg_ids, auto in updates:
+ meta = {
+ 'root': root,
+ 'destination': dest,
+ 'automerge': auto,
+ 'package_ids': pkg_ids,
+ }
+ cu = ConfigUpdate(source, meta, self)
+ config_updates.append(cu)
+ self._confc.notify_updates(config_updates)
+
def _unsupported_applications_signal(self, manual_package_ids,
package_ids):
const_debug_write(
@@ -1177,6 +1212,16 @@ class RigoServiceController(GObject.Object):
task.daemon = True
task.start()
+ def configuration_updates(self):
+ """
+ Request pending Configuration File Updates.
+ """
+ def _config():
+ return dbus.Interface(
+ self._entropy_bus,
+ dbus_interface=self.DBUS_INTERFACE).configuration_updates()
+ return self._execute_mainloop(_config)
+
def interrupt_activity(self):
"""
Interrupt any RigoDaemon activity.
diff --git a/rigo/rigo/enums.py b/rigo/rigo/enums.py
index 0ac597181..2d5b17354 100644
--- a/rigo/rigo/enums.py
+++ b/rigo/rigo/enums.py
@@ -36,26 +36,13 @@ class Icons:
MISSING_PKG = "dialog-question" # XXX: Not used?
GENERIC_MISSING = "gtk-missing-image"
INSTALLED_OVERLAY = "rigo-installed"
-
-# visibility of non applications in the search results
-class NonAppVisibility:
- (ALWAYS_VISIBLE,
- MAYBE_VISIBLE,
- NEVER_VISIBLE) = range (3)
+ CONFIGURATION_FILE = "text-plain"
# application actions
class AppActions:
INSTALL = "install"
REMOVE = "remove"
-# transaction types
-class TransactionTypes:
- INSTALL = "install"
- REMOVE = "remove"
- UPGRADE = "upgrade"
- APPLY = "apply_changes"
- REPAIR = "repair_dependencies"
-
from .version import VERSION, DISTRO, RELEASE, CODENAME
USER_AGENT="Entropy Rigo/%s (N;) %s/%s (%s)" % (
VERSION,
@@ -70,7 +57,8 @@ class RigoViewStates:
STATIC_VIEW_STATE,
APPLICATION_VIEW_STATE,
WORK_VIEW_STATE,
- ) = range(4)
+ CONFUPDATES_VIEW_STATE,
+ ) = range(5)
class LocalActivityStates:
(
diff --git a/rigo/rigo/models/application.py b/rigo/rigo/models/application.py
index a074f27ea..5aaccd669 100644
--- a/rigo/rigo/models/application.py
+++ b/rigo/rigo/models/application.py
@@ -107,33 +107,6 @@ class ReviewStats(object):
(self.app, self.ratings_average, self.downloads_total,
self.rating_spread, self.dampened_rating))
-class CategoryRowReference:
- """ A simple container for Category properties to be
- displayed in a AppListStore or AppTreeStore
- """
-
- def __init__(self, untranslated_name, display_name, subcats, pkg_count):
- self.untranslated_name = untranslated_name
- self.display_name = escape_markup(display_name)
- #self.subcategories = subcats
- self.pkg_count = pkg_count
- self.vis_count = pkg_count
- return
-
-class UncategorisedRowRef(CategoryRowReference):
-
- def __init__(self, untranslated_name=None, display_name=None, pkg_count=0):
- if untranslated_name is None:
- untranslated_name = 'Uncategorised'
- if display_name is None:
- display_name = _("Uncategorized")
-
- CategoryRowReference.__init__(self,
- untranslated_name,
- display_name,
- None, pkg_count)
- return
-
class ApplicationMetadata(object):
"""
This is the Entropy metadata manager for Application objects.
diff --git a/rigo/rigo/models/configupdate.py b/rigo/rigo/models/configupdate.py
new file mode 100644
index 000000000..94f32f95d
--- /dev/null
+++ b/rigo/rigo/models/configupdate.py
@@ -0,0 +1,62 @@
+# -*- 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 rigo.utils import escape_markup, prepare_markup
+
+class ConfigUpdate(object):
+
+ def __init__(self, source, metadata, rigo_service):
+ self._source = source
+ self._metadata = metadata
+ self._service = rigo_service
+
+ def source(self):
+ """
+ Return source file path (pointer to proposed
+ configuration file).
+ """
+ return self._source
+
+ def destination(self):
+ """
+ Return destination file path (pointer to file
+ to be replaced).
+ """
+ return self._metadata['destination']
+
+ def root(self):
+ """
+ Return current ROOT prefix (usually "").
+ """
+ return self._metadata['root']
+
+ def package_ids(self):
+ """
+ Return the list of package identifiers owning the
+ destination file.
+ """
+ return self._metadata['package_ids']
+
+ def get_markup(self):
+ """
+ Return ConfigurationUpdate markup text.
+ """
+ return escape_markup(
+ self.source() + " -> " + self.destination())
diff --git a/rigo/rigo/ui/gtk3/controllers/applications.py b/rigo/rigo/ui/gtk3/controllers/applications.py
index 2b3e85776..e31a4c3a9 100644
--- a/rigo/rigo/ui/gtk3/controllers/applications.py
+++ b/rigo/rigo/ui/gtk3/controllers/applications.py
@@ -55,7 +55,8 @@ class ApplicationsViewController(GObject.Object):
# View has been filled
"view-want-change" : (GObject.SignalFlags.RUN_LAST,
None,
- (GObject.TYPE_PYOBJECT,),
+ (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT,),
),
# User logged in to Entropy Web Services
"logged-in" : (GObject.SignalFlags.RUN_LAST,
@@ -288,12 +289,11 @@ class ApplicationsViewController(GObject.Object):
return
elif text == "rigo:vte":
GLib.idle_add(self.emit, "view-want-change",
- RigoViewStates.WORK_VIEW_STATE)
+ RigoViewStates.WORK_VIEW_STATE,
+ None)
return
- elif text == "rigo:output":
- GLib.idle_add(self.emit, "view-want-change",
- RigoViewStates.WORK_VIEW_STATE)
- GLib.idle_add(self._service.output_test)
+ elif text == "rigo:confupdate":
+ self._service.configuration_updates()
return
elif text.startswith("rigo:simulate:i:"):
sim_str = text[len("rigo:simulate:i:"):].strip()
diff --git a/rigo/rigo/ui/gtk3/controllers/confupdate.py b/rigo/rigo/ui/gtk3/controllers/confupdate.py
new file mode 100644
index 000000000..bd1c5f0c3
--- /dev/null
+++ b/rigo/rigo/ui/gtk3/controllers/confupdate.py
@@ -0,0 +1,114 @@
+# -*- 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 GObject
+
+from rigo.ui.gtk3.widgets.notifications import \
+ ConfigUpdatesNotificationBox
+
+class ConfigUpdatesViewController(GObject.Object):
+
+ def __init__(self, entropy_client, config_store, config_view):
+ GObject.Object.__init__(self)
+ self._entropy = entropy_client
+ self._store = config_store
+ self._view = config_view
+ self._nc = None
+ self._avc = None
+
+ 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_updates(self, config_updates):
+ """
+ Notify Configuration File Updates to User.
+ """
+ # setup store
+ self.set_many(config_updates)
+ if self._nc is not None and self._avc is not None:
+ box = ConfigUpdatesNotificationBox(
+ self._entropy, self._avc, len(config_updates))
+ self._nc.append(box)
diff --git a/rigo/rigo/ui/gtk3/models/confupdateliststore.py b/rigo/rigo/ui/gtk3/models/confupdateliststore.py
new file mode 100644
index 000000000..b02c10997
--- /dev/null
+++ b/rigo/rigo/ui/gtk3/models/confupdateliststore.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2012 Fabio Erculiani
+
+Authors:
+ Fabio Erculiani
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+details.
+
+You should have received a copy of the GNU General Public License along with
+this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+from gi.repository import Gtk, GObject, GLib
+
+
+class ConfigUpdatesListStore(Gtk.ListStore):
+
+ # ConfigUpdate object
+ COL_TYPES = (GObject.TYPE_PYOBJECT,)
+
+ ICON_SIZE = 48
+
+ __gsignals__ = {
+ # Redraw signal, requesting UI update
+ "redraw-request" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ tuple(),
+ ),
+ }
+
+ def __init__(self):
+ Gtk.ListStore.__init__(self)
+ self.set_column_types(self.COL_TYPES)
+
diff --git a/rigo/rigo/ui/gtk3/widgets/apptreeview.py b/rigo/rigo/ui/gtk3/widgets/apptreeview.py
index ba4edb8a6..36532836a 100644
--- a/rigo/rigo/ui/gtk3/widgets/apptreeview.py
+++ b/rigo/rigo/ui/gtk3/widgets/apptreeview.py
@@ -21,7 +21,6 @@ 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
-import logging
import os
from entropy.i18n import _
@@ -31,7 +30,7 @@ from cellrenderers import CellRendererAppView, CellButtonRenderer, \
from rigo.em import em, StockEms
from rigo.enums import Icons, AppActions
-from rigo.models.application import CategoryRowReference, Application
+from rigo.models.application import Application
from RigoDaemon.enums import AppActions as DaemonAppActions
@@ -50,7 +49,6 @@ class AppTreeView(Gtk.TreeView):
def __init__(self, entropy_client, backend, apc, icons, show_ratings,
icon_size, store=None):
Gtk.TreeView.__init__(self)
- self._logger = logging.getLogger(__name__)
self._entropy = entropy_client
self._apc = apc
@@ -66,8 +64,9 @@ class AppTreeView(Gtk.TreeView):
try:
self.set_property("ubuntu-almost-fixed-height-mode", True)
self.set_fixed_height_mode(True)
- except:
- self._logger.warn("ubuntu-almost-fixed-height-mode extension not available")
+ print("MEEP")
+ except Exception:
+ pass
self.set_headers_visible(False)
@@ -189,9 +188,6 @@ class AppTreeView(Gtk.TreeView):
if path == None: return None
return model[path][COL_ROW_DATA]
- def rowref_is_category(self, rowref):
- return isinstance(rowref, CategoryRowReference)
-
def _calc_row_heights(self, tr):
ypad = StockEms.SMALL
tr.set_property('xpad', StockEms.MEDIUM)
@@ -228,10 +224,6 @@ class AppTreeView(Gtk.TreeView):
if not rowref:
return
- if self.rowref_is_category(rowref):
- window.set_cursor(None)
- return
-
use_hand = False
for btn in tr.get_buttons():
if btn.state == Gtk.StateFlags.INSENSITIVE:
@@ -261,11 +253,8 @@ class AppTreeView(Gtk.TreeView):
rowref = self.get_rowref(model, path)
if not rowref: return
- if self.has_focus(): self.grab_focus()
-
- if self.rowref_is_category(rowref):
- self.expand_path(None)
- return
+ if self.has_focus():
+ self.grab_focus()
sel.select_path(path)
self._update_selected_row(view, tr, path)
@@ -279,18 +268,11 @@ class AppTreeView(Gtk.TreeView):
if not rows:
return False
row = rows[0]
- if self.rowref_is_category(row):
- return False
# update active app, use row-ref as argument
self.expand_path(row)
-
pkg_match = model[row][COL_ROW_DATA]
- # make sure this is not a category (LP: #848085)
- if self.rowref_is_category(pkg_match):
- return False
-
action_btn = tr.get_button_by_name(
CellButtonIDs.ACTION)
#if not action_btn: return False
@@ -338,8 +320,6 @@ class AppTreeView(Gtk.TreeView):
if not rowref:
return
- if self.rowref_is_category(rowref): return
-
x, y = self.get_pointer()
for btn in tr.get_buttons():
if btn.point_in(x, y):
@@ -359,9 +339,7 @@ class AppTreeView(Gtk.TreeView):
# check the path is valid and is not a category row
path = res[0]
- is_cat = self.rowref_is_category(
- self.get_rowref(view.get_model(), path))
- if path is None or is_cat:
+ if path is None:
return False
# only act when the selection is already there
@@ -483,13 +461,8 @@ class AppTreeView(Gtk.TreeView):
return
def _app_activated_cb(self, btn, btn_id, pkg_match, store, path):
- if self.rowref_is_category(pkg_match):
- return
- # FIXME: would be nice if that would be more elegant
- # because we use a treefilter we need to get the "real"
- # model first
- if type(store) is Gtk.TreeModelFilter:
+ if isinstance(store, Gtk.TreeModelFilter):
store = store.get_model()
app = self.appmodel.get_application(pkg_match)
diff --git a/rigo/rigo/ui/gtk3/widgets/cellrenderers.py b/rigo/rigo/ui/gtk3/widgets/cellrenderers.py
index 73ce0af79..d84ee17e9 100644
--- a/rigo/rigo/ui/gtk3/widgets/cellrenderers.py
+++ b/rigo/rigo/ui/gtk3/widgets/cellrenderers.py
@@ -23,9 +23,11 @@ this program; if not, write to the Free Software Foundation, Inc.,
"""
from gi.repository import Gtk, Gdk, GObject, Pango
+from threading import Lock
+
from rigo.em import Ems
-from rigo.models.application import CategoryRowReference
from rigo.utils import escape_markup
+from rigo.enums import Icons
from stars import StarRenderer, StarSize
@@ -53,14 +55,13 @@ class CellRendererAppView(Gtk.CellRendererText):
__gproperties__ = {
'application' : (GObject.TYPE_PYOBJECT, 'document',
- 'a xapian document containing pkg information',
+ 'an Entropy Package Match',
GObject.PARAM_READWRITE),
- 'isactive' : (bool, 'isactive', 'is cell active/selected', False,
+ 'isactive' : (bool,'isactive', 'is cell active/selected', False,
GObject.PARAM_READWRITE),
}
-
def __init__(self, icons, layout, show_ratings, overlay_icon_name):
GObject.GObject.__init__(self)
@@ -100,26 +101,6 @@ class CellRendererAppView(Gtk.CellRendererText):
def _layout_get_pixel_height(self, layout):
return layout.get_size()[1] / Pango.SCALE
- def _render_category(self,
- context, cr, app, cell_area, layout, xpad, ypad, is_rtl):
-
- layout.set_markup('%s' % app.display_name, -1)
-
- # work out max allowable layout width
- lw = self._layout_get_pixel_width(layout)
- lh = self._layout_get_pixel_height(layout)
-
- if not is_rtl:
- x = cell_area.x
- else:
- x = cell_area.x + cell_area.width - lw
- y = cell_area.y + (cell_area.height - lh)/2
- #w = cell_area.width
- #h = cell_area.height
-
- Gtk.render_layout(context, cr, x, y, layout)
- return
-
def _render_icon(self, cr, app, cell_area, xpad, ypad, is_rtl):
# calc offsets so icon is nicely centered
icon = self.model.get_icon(app)
@@ -359,28 +340,8 @@ class CellRendererAppView(Gtk.CellRendererText):
star_width, star_height = self._stars.get_visible_size(context)
is_rtl = widget.get_direction() == Gtk.TextDirection.RTL
- # important! ensures correct text rendering, esp. when using hicolor theme
- #~ if (flags & Gtk.CellRendererState.SELECTED) != 0:
- #~ # this follows the behaviour that gtk+ uses for states in treeviews
- #~ if widget.has_focus():
- #~ state = Gtk.StateFlags.SELECTED
- #~ else:
- #~ state = Gtk.StateFlags.ACTIVE
- #~ else:
- #~ state = Gtk.StateFlags.NORMAL
-
layout = self._layout
-
context.save()
- #~ context.set_state(state)
-
- if isinstance(app, CategoryRowReference):
- self._render_category(context, cr, app,
- cell_area,
- layout,
- xpad, ypad,
- is_rtl)
- return
self._render_icon(cr, app,
cell_area,
@@ -428,6 +389,223 @@ class CellRendererAppView(Gtk.CellRendererText):
return
+class ConfigUpdateCellButtonIDs:
+
+ EDIT = 0
+ DIFF = 2
+ MERGE = 3
+ DISCARD = 4
+
+
+class CellRendererConfigUpdateView(Gtk.CellRendererText):
+
+ _ICON = None
+ _ICON_MUTEX = Lock()
+
+ __gproperties__ = {
+ 'confupdate' : (GObject.TYPE_PYOBJECT, 'document',
+ 'a ConfigUpdate 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.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 CellRendererConfigUpdateView._ICON is not None:
+ return CellRendererConfigUpdateView._ICON
+ with CellRendererConfigUpdateView._ICON_MUTEX:
+ if CellRendererConfigUpdateView._ICON is not None:
+ return CellRendererConfigUpdateView._ICON
+ _icon = self._icons.load_icon(
+ Icons.CONFIGURATION_FILE,
+ self._icon_size, 0)
+ CellRendererConfigUpdateView._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 _render_summary(self, context, cr, cu,
+ cell_area, layout, xpad, ypad,
+ is_rtl):
+
+ layout.set_markup(cu.get_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):
+ cu = self.props.confupdate
+ if not cu:
+ return
+
+ self.model = widget.confmodel
+ 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, cu,
+ cell_area,
+ xpad, ypad,
+ is_rtl)
+
+ self._render_summary(context, cr, cu,
+ 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
+
+
class CellButtonRenderer:
def __init__(self, widget, name, use_max_variant_width=True):
diff --git a/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py b/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py
new file mode 100644
index 000000000..692f712ab
--- /dev/null
+++ b/rigo/rigo/ui/gtk3/widgets/confupdatetreeview.py
@@ -0,0 +1,369 @@
+# -*- 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, \
+ CellRendererConfigUpdateView, ConfigUpdateCellButtonIDs
+
+from rigo.em import em, StockEms, Ems
+from rigo.ui.gtk3.models.confupdateliststore import ConfigUpdatesListStore
+
+
+class ConfigUpdatesTreeView(Gtk.TreeView):
+
+ __gsignals__ = {
+ # Source configuration file edit signal
+ "source-edit" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # Show diff signal
+ "show-diff" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # Merge source configuration file signal
+ "source-merge" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # Discard source configuration file signal
+ "source-discard" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ }
+
+ VARIANT_EDIT = 0
+ 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))
+
+ # create buttons and set initial strings
+ edit_source = CellButtonRenderer(
+ self, name=ConfigUpdateCellButtonIDs.EDIT)
+ edit_source.set_markup_variants(
+ {self.VARIANT_EDIT: _("Edit")})
+ tr.button_pack_start(edit_source)
+
+ edit_dest = CellButtonRenderer(
+ self, name=ConfigUpdateCellButtonIDs.DIFF)
+ edit_dest.set_markup_variants(
+ {self.VARIANT_DIFF: _("Difference")})
+ tr.button_pack_start(edit_dest)
+
+ merge = CellButtonRenderer(
+ self, name=ConfigUpdateCellButtonIDs.MERGE)
+ merge.set_markup_variants(
+ {self.VARIANT_MERGE: _("Accept")})
+ tr.button_pack_end(merge)
+
+ discard = CellButtonRenderer(
+ self, name=ConfigUpdateCellButtonIDs.DISCARD)
+ discard.set_markup_variants(
+ {self.VARIANT_DISCARD: _("Discard")})
+ tr.button_pack_end(discard)
+
+ column = Gtk.TreeViewColumn("ConfigUpdates", tr,
+ confupdate=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("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
+ # FIXME: show source?
+
+ 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
+
+ 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)
+
+ 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 _confupdate_activated_cb(self, btn, btn_id, cu, store, path):
+
+ if isinstance(store, Gtk.TreeModelFilter):
+ store = store.get_model()
+
+ if btn_id == ConfigUpdateCellButtonIDs.EDIT:
+ self.emit("source-edit", cu)
+ elif btn_id == ConfigUpdateCellButtonIDs.DIFF:
+ self.emit("show-diff", cu)
+ elif btn_id == ConfigUpdateCellButtonIDs.MERGE:
+ self.emit("source-merge", cu)
+ elif btn_id == ConfigUpdateCellButtonIDs.DISCARD:
+ self.emit("source-discard", cu)
+ 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 _xy_is_over_focal_row(self, x, y):
+ res = self.get_path_at_pos(x, y)
+ #cur = self.get_cursor()
+ if not res:
+ return False
+ return self.get_path_at_pos(x, y)[0] == self.get_cursor()[0]
diff --git a/rigo/rigo/ui/gtk3/widgets/notifications.py b/rigo/rigo/ui/gtk3/widgets/notifications.py
index e27502648..c60a5130c 100644
--- a/rigo/rigo/ui/gtk3/widgets/notifications.py
+++ b/rigo/rigo/ui/gtk3/widgets/notifications.py
@@ -30,7 +30,7 @@ from rigo.em import StockEms
from rigo.utils import build_register_url, open_url, escape_markup, \
prepare_markup
from rigo.models.application import Application
-from rigo.enums import AppActions, LocalActivityStates
+from rigo.enums import AppActions, LocalActivityStates, RigoViewStates
from entropy.const import etpConst, const_convert_to_unicode, \
const_debug_write
@@ -184,8 +184,9 @@ class UpdatesNotificationBox(NotificationBox):
msg += ". " + _("What to do?")
- NotificationBox.__init__(self, msg,
- tooltip=_("Updates available, how about installing them?"),
+ NotificationBox.__init__(self, prepare_markup(msg),
+ tooltip=prepare_markup(
+ _("Updates available, how about installing them?")),
message_type=Gtk.MessageType.WARNING,
context_id="UpdatesNotificationBox")
self.add_button(_("_Update System"), self._update)
@@ -226,7 +227,7 @@ class RepositoriesUpdateNotificationBox(NotificationBox):
msg = _("Repositories should be downloaded, update now?")
NotificationBox.__init__(self, msg,
- tooltip=_("I dunno dude, I'd say Yes"),
+ tooltip=prepare_markup(_("I dunno dude, I'd say Yes")),
message_type=Gtk.MessageType.ERROR,
context_id="RepositoriesUpdateNotificationBox")
self.add_button(_("_Yes, why not?"), self._update)
@@ -269,7 +270,8 @@ class LoginNotificationBox(NotificationBox):
NotificationBox.__init__(self, None,
message_widget=self._make_login_box(),
- tooltip=_("You need to login to Entropy Web Services"),
+ tooltip=prepare_markup(
+ _("You need to login to Entropy Web Services")),
message_type=Gtk.MessageType.WARNING,
context_id=context_id)
@@ -390,7 +392,7 @@ class ConnectivityNotificationBox(NotificationBox):
"are you connected to the interweb?")
NotificationBox.__init__(self, msg,
- tooltip=_("Don't ask me..."),
+ tooltip=prepare_markup(_("Don't ask me...")),
message_type=Gtk.MessageType.ERROR,
context_id="ConnectivityNotificationBox")
self.add_destroy_button(_("_Of course not"))
@@ -401,7 +403,7 @@ class PleaseWaitNotificationBox(NotificationBox):
def __init__(self, message, context_id):
NotificationBox.__init__(self, message,
- tooltip=_("A watched pot never boils"),
+ tooltip=prepare_markup(_("A watched pot never boils")),
message_type=Gtk.MessageType.INFO,
context_id=context_id)
self._spinner = Gtk.Spinner()
@@ -482,7 +484,8 @@ class LicensesNotificationBox(NotificationBox):
NotificationBox.__init__(
self, None, message_widget=label,
- tooltip=_("Make sure to review all the licenses"),
+ tooltip=prepare_markup(
+ _("Make sure to review all the licenses")),
message_type=Gtk.MessageType.WARNING,
context_id="LicensesNotificationBox")
@@ -950,3 +953,45 @@ class QueueActionNotificationBox(NotificationBox):
This NotificationBox cannot be destroyed easily.
"""
return True
+
+
+class ConfigUpdatesNotificationBox(NotificationBox):
+
+ def __init__(self, entropy_client, avc, updates_len):
+ self._entropy = entropy_client
+ self._avc = avc
+
+ msg = ngettext("There is %d configuration file update",
+ "There are %d configuration file updates",
+ updates_len)
+ msg = msg % (updates_len,)
+
+ msg += ".\n\n"
+ msg += _("It is extremely important to"
+ " update these configuration files before"
+ " rebooting the System.")
+ msg += ""
+
+ context_id = "ConfigUpdatesNotificationContextId"
+ NotificationBox.__init__(
+ self, prepare_markup(msg),
+ message_type=Gtk.MessageType.WARNING,
+ context_id=context_id)
+
+ self.add_button(_("Let me see"), self._on_show_me)
+ self.add_destroy_button(_("Happily ignore"))
+
+ def _on_show_me(self, widget):
+ """
+ Show the proposed configuration file updates
+ """
+ self._avc.emit(
+ "view-want-change",
+ RigoViewStates.CONFUPDATES_VIEW_STATE,
+ None)
+
+ def is_managed(self):
+ """
+ This NotificationBox cannot be destroyed easily.
+ """
+ return True
diff --git a/rigo/rigo_app.py b/rigo/rigo_app.py
index 1245b10c0..f1ee684a6 100644
--- a/rigo/rigo_app.py
+++ b/rigo/rigo_app.py
@@ -41,11 +41,14 @@ from rigo.paths import DATA_DIR
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.notifications import NotificationBox
from rigo.ui.gtk3.controllers.applications import \
ApplicationsViewController
from rigo.ui.gtk3.controllers.application import \
ApplicationViewController
+from rigo.ui.gtk3.controllers.confupdate import \
+ ConfigUpdatesViewController
from rigo.ui.gtk3.controllers.notifications import \
UpperNotificationViewController, BottomNotificationViewController
@@ -53,6 +56,7 @@ from rigo.ui.gtk3.controllers.work import \
WorkViewController
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.utils import init_sc_css_provider, get_sc_icon_theme
from rigo.utils import escape_markup
@@ -110,6 +114,9 @@ class Rigo(Gtk.Application):
RigoViewStates.WORK_VIEW_STATE: (
self._enter_work_state,
self._exit_work_state),
+ RigoViewStates.CONFUPDATES_VIEW_STATE: (
+ self._enter_confupdates_state,
+ self._exit_confupdates_state,)
}
self._state_mutex = Lock()
@@ -136,6 +143,12 @@ class Rigo(Gtk.Application):
self._app_view_port = self._builder.get_object("appViewVport")
self._app_view_port.set_name("rigo-view")
self._not_found_box = self._builder.get_object("appsViewNotFoundVbox")
+
+ self._config_scrolled_view = self._builder.get_object(
+ "configViewScrolledWindow")
+ self._config_view = self._builder.get_object("configViewVbox")
+ self._config_view.set_name("rigo-view")
+
self._search_entry = self._builder.get_object("searchEntry")
self._search_entry_completion = self._builder.get_object(
"searchEntryCompletion")
@@ -168,6 +181,18 @@ class Rigo(Gtk.Application):
self._app_view_c.connect("application-show",
self._on_application_show)
+ # Configuration file updates model, view and controller
+ self._config_store = ConfigUpdatesListStore()
+ self._view_config = ConfigUpdatesTreeView(
+ icons, ConfigUpdatesListStore.ICON_SIZE)
+ self._config_scrolled_view.add(self._view_config)
+ def _config_queue_draw(*args):
+ self._view_config.queue_draw()
+ self._config_store.connect("redraw-request", _config_queue_draw)
+ self._config_view_c = ConfigUpdatesViewController(
+ self._entropy, self._config_store, self._view_config)
+ self._service.set_configuration_controller(self._config_view_c)
+
self._welcome_box = WelcomeBox()
settings = Gtk.Settings.get_default()
@@ -208,6 +233,9 @@ class Rigo(Gtk.Application):
self._app_view_c.set_notification_controller(self._nc)
self._app_view_c.set_applications_controller(self._avc)
+ self._config_view_c.set_notification_controller(self._nc)
+ self._config_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)
@@ -349,8 +377,8 @@ class Rigo(Gtk.Application):
def _on_view_filled(self, *args):
self._change_view_state(RigoViewStates.BROWSER_VIEW_STATE)
- def _on_view_change(self, widget, state):
- self._change_view_state(state)
+ def _on_view_change(self, widget, state, payload):
+ self._change_view_state(state, payload=payload)
def _on_application_show(self, *args):
self._change_view_state(RigoViewStates.APPLICATION_VIEW_STATE)
@@ -369,6 +397,20 @@ class Rigo(Gtk.Application):
"""
self._apps_view.show()
+ def _exit_confupdates_state(self):
+ """
+ Action triggered when UI exits the Configuration Updates
+ state (or mode).
+ """
+ self._config_view.hide()
+
+ def _enter_confupdates_state(self):
+ """
+ Action triggered when UI enters the Configuration Updates
+ state (or mode).
+ """
+ self._config_view.show()
+
def _exit_static_state(self):
"""
Action triggered when UI exits the Static Browser
@@ -427,7 +469,8 @@ class Rigo(Gtk.Application):
"""
self._work_view.hide()
- def _change_view_state(self, state, lock=False, _ignore_lock=False):
+ def _change_view_state(self, state, lock=False, _ignore_lock=False,
+ payload=None):
"""
Change Rigo Application UI state.
You can pass a custom widget that will be shown in case
@@ -444,8 +487,9 @@ class Rigo(Gtk.Application):
raise AttributeError("wrong view state")
enter_st, exit_st = txc
- current_enter_st, current_exit_st = self._state_transactions.get(
- self._current_state)
+ current_enter_st, current_exit_st = \
+ self._state_transactions.get(
+ self._current_state)
# exit from current state
current_exit_st()
# enter the new state
@@ -615,6 +659,7 @@ class Rigo(Gtk.Application):
return
self._thread_dumper()
+ self._config_view_c.setup()
self._app_view_c.setup()
self._avc.setup()
self._nc.setup()