[Rigo/RigoDaemon] partially implement configuration file updates management
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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:
|
||||
(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user