[Rigo/RigoDaemon] partially implement configuration file updates management

This commit is contained in:
Fabio Erculiani
2012-04-07 19:33:28 +02:00
parent c5ab6083ea
commit 8a34b071b0
18 changed files with 1363 additions and 146 deletions
+288
View File
@@ -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):
+27
View File
@@ -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
+1
View File
@@ -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"
+29
View File
@@ -31,6 +31,35 @@
<arg name="length" type="i" direction="out"/>
</method>
<method name="merge_configuration">
<arg name="source" type="s" direction="in"/>
<arg name="accepted" type="b" direction="out"/>
</method>
<method name="diff_configuration">
<arg name="source" type="s" direction="in"/>
<arg name="path" type="s" direction="out"/>
</method>
<method name="view_configuration_source">
<arg name="source" type="s" direction="in"/>
<arg name="path" type="s" direction="out"/>
</method>
<method name="view_configuration_destination">
<arg name="destination" type="s" direction="in"/>
<arg name="path" type="s" direction="out"/>
</method>
<method name="discard_configuration">
<arg name="source" type="s" direction="in"/>
<arg name="accepted" type="b" direction="out"/>
</method>
<method name="configuration_updates"/>
<method name="reload_configuration_updates"/>
<method name="action">
<arg name="package_id" type="i" direction="in"/>
<arg name="repository_id" type="s" direction="in"/>
@@ -41,4 +41,17 @@
</defaults>
</action>
<action id="org.sabayon.RigoDaemon.configuration">
<description>Manage Configuration</description>
<message>
Authentication is required to Manage System Configuration
</message>
<icon_name>package-x-generic</icon_name>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
+27 -2
View File
@@ -75,7 +75,6 @@
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="appsViewScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
@@ -137,6 +136,32 @@
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="configViewVbox">
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="configViewScrolledWindow">
<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">4</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="appViewScrollWin">
<property name="can_focus">True</property>
@@ -522,7 +547,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
</object>
+49 -4
View File
@@ -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.
+3 -15
View File
@@ -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:
(
-27
View File
@@ -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.
+62
View File
@@ -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())
@@ -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()
+114
View File
@@ -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)
@@ -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)
+8 -35
View File
@@ -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)
+222 -44
View File
@@ -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('<b>%s</b>' % 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):
@@ -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]
+53 -8
View File
@@ -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, <b>update now</b>?")
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 <b>interweb</b>?")
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 <b>%d</b> configuration file update",
"There are <b>%d</b> configuration file updates",
updates_len)
msg = msg % (updates_len,)
msg += ".\n\n<small>"
msg += _("It is <b>extremely</b> important to"
" update these configuration files before"
" <b>rebooting</b> the System.")
msg += "</small>"
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
+50 -5
View File
@@ -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()