diff --git a/rigo/data/ui/gtk3/rigo.ui b/rigo/data/ui/gtk3/rigo.ui index 0e75608d2..a0481287d 100644 --- a/rigo/data/ui/gtk3/rigo.ui +++ b/rigo/data/ui/gtk3/rigo.ui @@ -112,7 +112,7 @@ - + False @@ -124,6 +124,19 @@ 2 + + + False + + + + + + True + True + 3 + + True @@ -508,7 +521,7 @@ True True - 3 + 4 diff --git a/rigo/rigo/ui/gtk3/widgets/terminal.py b/rigo/rigo/ui/gtk3/widgets/terminal.py new file mode 100644 index 000000000..9b7eddd25 --- /dev/null +++ b/rigo/rigo/ui/gtk3/widgets/terminal.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2012 Fabio Erculiani + +Authors: + Fabio Erculiani + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +import os + +from gi.repository import Vte + +from entropy.const import etpConst, const_isunicode + +class TerminalWidget(Vte.Terminal): + + """ + Rigo Terminal Widget. + """ + + def __init__(self): + # set $TERM variable, avoid shell scripts to complain + os.environ['TERM'] = "xterm" + Vte.Terminal.__init__(self) + + def _configure(self): + self.set_emulation("xterm") + self.set_background_saturation(0.0) + self.set_opacity(65535) + self.get_font().set_size(9000) + self.set_scrollback_lines(10000) + + def reset(self): + Vte.Terminal.reset(self, True, True) + self._configure() + + def feed_child(self, txt): + # Workaround vte.Terminal bug not passing to .feed proper message RAW + # size. feed() supports UTF-8 but then, string length is wrongly passed + # by python, because it does not consider the fact that UTF-8 chars can + # be 16bits long. + raw_txt_len = len(txt) + if const_isunicode(txt): + raw_txt_len = len(txt.encode(etpConst['conf_encoding'])) + + return Vte.Terminal.feed(self, txt, raw_txt_len) diff --git a/rigo/rigo_app.py b/rigo/rigo_app.py index 94ae5f087..2d06f6284 100644 --- a/rigo/rigo_app.py +++ b/rigo/rigo_app.py @@ -26,15 +26,18 @@ import tempfile import time from threading import Lock +import dbus + sys.path.insert(0, "../lib") sys.path.insert(1, "../client") sys.path.insert(2, "./") sys.path.insert(3, "/usr/lib/entropy/lib") sys.path.insert(4, "/usr/lib/entropy/client") sys.path.insert(5, "/usr/lib/entropy/rigo") +sys.path.insert(6, "/usr/lib/rigo") -from gi.repository import Gtk, Gdk, Gio, GLib, GObject +from gi.repository import Gtk, Gdk, Gio, GLib, GObject, Vte, Pango from rigo.paths import DATA_DIR from rigo.enums import Icons, AppActions @@ -47,22 +50,489 @@ from rigo.ui.gtk3.widgets.notifications import NotificationBox, \ from rigo.ui.gtk3.widgets.welcome import WelcomeBox from rigo.ui.gtk3.widgets.stars import ReactiveStar from rigo.ui.gtk3.widgets.comments import CommentBox +from rigo.ui.gtk3.widgets.terminal import TerminalWidget from rigo.ui.gtk3.widgets.images import ImageBox from rigo.ui.gtk3.models.appliststore import AppListStore from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme from rigo.utils import build_application_store_url, build_register_url, \ escape_markup, prepare_markup +from RigoDaemon.enums import ActivityStates + from entropy.const import etpConst, etpUi, const_debug_write, \ - const_debug_enabled, const_convert_to_unicode + const_debug_enabled, const_convert_to_unicode, const_isunicode from entropy.client.interfaces import Client from entropy.client.interfaces.repository import Repository from entropy.services.client import WebService -from entropy.misc import TimeScheduled, ParallelTask +from entropy.misc import TimeScheduled, ParallelTask, ReadersWritersSemaphore from entropy.i18n import _, ngettext +from entropy.output import darkgreen, brown, darkred, red, blue import entropy.tools +class RigoServiceController(GObject.Object): + + """ + This is the Rigo Application frontend to Rigo Daemon. + Handles privileged requests on our behalf. + """ + + __gsignals__ = { + # we request to lock the whole UI wrt repo + # interaction + "repositories-updating" : (GObject.SignalFlags.RUN_LAST, + None, + (GObject.TYPE_PYOBJECT,), + ), + # View has been filled + "repositories-updated" : (GObject.SignalFlags.RUN_LAST, + None, + tuple(), + ), + } + + DBUS_INTERFACE = "org.sabayon.Rigo" + DBUS_PATH = "/daemon" + _OUTPUT_SIGNAL = "output" + _REPOSITORIES_UPDATED_SIGNAL = "repositories_updated" + _TRANSFER_OUTPUT_SIGNAL = "transfer_output" + + def __init__(self, rigo_app, activity_rwsem, entropy_client, entropy_ws): + GObject.Object.__init__(self) + self._rigo = rigo_app + self._activity_rwsem = activity_rwsem + self._activity_rwsem_acquired_mutex = Lock() + self._activity_rwsem_acquired_by_us = False + self._nc = None + self._wc = None + self._avc = None + self._terminal = None + self._entropy = entropy_client + self._entropy_ws = entropy_ws + self.__dbus_main_loop = None + self.__system_bus = None + self.__entropy_bus = None + self.__entropy_bus_mutex = Lock() + self._connected_mutex = Lock() + self._connected = False + + def set_applications_controller(self, avc): + """ + Bind ApplicationsViewController object to this class. + """ + self._avc = avc + + def set_terminal(self, terminal): + """ + Bind a TerminalWidget to this object, in order to be used with + events coming from dbus. + """ + self._terminal = terminal + + def set_work_controller(self, wc): + """ + Bind a WorkViewController to this object in order to be used to + set progress status. + """ + self._wc = wc + + def set_notification_controller(self, nc): + """ + Bind a NotificationViewController to this object. + """ + self._nc = nc + + @property + def _dbus_main_loop(self): + if self.__dbus_main_loop is None: + from dbus.mainloop.glib import DBusGMainLoop + self.__dbus_main_loop = DBusGMainLoop(set_as_default=True) + return self.__dbus_main_loop + + @property + def _system_bus(self): + if self.__system_bus is None: + self.__system_bus = dbus.SystemBus( + mainloop=self._dbus_main_loop) + return self.__system_bus + + @property + def _entropy_bus(self): + with self.__entropy_bus_mutex: + if self.__entropy_bus is None: + self.__entropy_bus = self._system_bus.get_object( + self.DBUS_INTERFACE, self.DBUS_PATH + ) + self.__entropy_bus.connect_to_signal( + self._OUTPUT_SIGNAL, self._output_signal, + dbus_interface=self.DBUS_INTERFACE) + self.__entropy_bus.connect_to_signal( + self._REPOSITORIES_UPDATED_SIGNAL, + self._repositories_updated_signal, + dbus_interface=self.DBUS_INTERFACE) + self.__entropy_bus.connect_to_signal( + self._TRANSFER_OUTPUT_SIGNAL, + self._transfer_output_signal, + dbus_interface=self.DBUS_INTERFACE) + return self.__entropy_bus + + def _repositories_updated_signal(self, request_id, result, message): + """ + Signal coming from RigoDaemon notifying us that repositories have + been updated. + """ + self._release_local_resources() + # 1 -- ACTIVITY CRIT :: OFF + with self._activity_rwsem_acquired_mutex: + # this signal comes from RigoDaemon but might not be + # caused by us + if self._activity_rwsem_acquired_by_us: + self._activity_rwsem_acquired_by_us = False + self._activity_rwsem.writer_release() + + self.emit("repositories-updated") + if const_debug_enabled(): + const_debug_write(__name__, "RigoServiceController: unlock-ui") + + def _output_signal(self, text, header, footer, back, importance, level, + count_c, count_t, percent): + """ + Entropy Client output() method from RigoDaemon comes here. + Will be redirected to a virtual terminal here in Rigo. + This is called in the Gtk.MainLoop. + """ + if count_c == 0 and count_t == 0: + count = None + else: + count = (count_c, count_t) + + if self._terminal is None: + self._entropy.output(text, header=header, footer=footer, + back=back, importance=importance, + level=level, count=count, + percent=percent) + return + + color_func = darkgreen + if level == "warning": + color_func = brown + elif level == "error": + color_func = darkred + + count_str = "" + if count: + if len(count) > 1: + if percent: + fraction = float(count[0])/count[1] + percent_str = str(round(fraction*100, 1)) + count_str = " ("+percent_str+"%) " + else: + count_str = " (%s/%s) " % (red(str(count[0])), + blue(str(count[1])),) + + # reset cursor + self._terminal.feed_child(chr(27) + '[2K') + if back: + msg = "\r" + color_func(">>") + " " + header + count_str + text \ + + footer + else: + msg = "\r" + color_func(">>") + " " + header + count_str + text \ + + footer + "\r\n" + + self._terminal.feed_child(msg) + + def _transfer_output_signal(self, average, downloaded_size, total_size, + data_transfer_bytes, time_remaining_secs): + """ + Entropy UrlFetchers update() method (via transfer_output()) from + RigoDaemon comes here. Will be redirected to WorkAreaController + Progress Bar if available. + """ + if self._wc is None: + return + + fraction = float(average) / 100 + + human_dt = entropy.tools.bytes_into_human(data_transfer_bytes) + + text = "%s/%s kB @ %s/sec, %s" % ( + round(float(downloaded_size)/1024, 1), + round(total_size, 1), + human_dt, time_remaining_secs) + + self._wc.set_progress(fraction, text=text) + + def activity(self): + """ + Return RigoDaemon activity states (any of RigoDaemon.ActivityStates + values). + """ + return dbus.Interface( + self._entropy_bus, + dbus_interface=self.DBUS_INTERFACE).activity() + + def output_test(self): + """ + Test Output Signaling. Will be removed in future Rigo versions. + """ + if self._wc is not None: + self._wc.activate_progress_bar() + self._wc.activate_app_box() + dbus.Interface( + self._entropy_bus, + dbus_interface=self.DBUS_INTERFACE).output_test() + + def output_test_safe(self): + """ + Same as output_test() but thread-safe. + """ + GLib.idle_add(self.output_test) + + def _connect(self): + """ + Inform RigoDaemon that a new Client is now connected. + This MUST be called after Entropy Resources Lock is + acquired in exclusive mode. + """ + with self._connected_mutex: + if self._connected: + return + # inform daemon that a new instance is now connected + dbus.Interface( + self._entropy_bus, + dbus_interface=self.DBUS_INTERFACE).connect() + self._connected = True + + def _release_local_resources(self): + """ + Release all the local resources (like repositories) + that shall be used by RigoDaemon. + For example, leaving EntropyRepository objects open + would cause sqlite3 to deadlock. + """ + self._avc.clear_safe() + self._entropy.close_repositories() + + def _scale_up(self): + """ + Acquire (in blocking mode) the Entropy Resources Lock + in exclusive mode. Scale up privileges, and ask for + root password if not done yet. + """ + # FIXME, complete, need to be nice and not block, etc + # FIXME, ask for password. + self._entropy.promote_resources(blocking=True) + self._connect() + return True + + def _update_repositories(self, repositories, force): + """ + Ask RigoDaemon to update repositories once we're + 100% sure that the UI is locked down. + """ + while not self._rigo.is_ui_locked(): + if const_debug_enabled(): + const_debug_write(__name__, "RigoServiceController: " + "waiting Rigo UI lock") + time.sleep(0.5) + + if const_debug_enabled(): + const_debug_write(__name__, "RigoServiceController: " + "rigo UI now locked!") + + # 1 -- ACTIVITY CRIT :: ON + self._activity_rwsem.writer_acquire() + with self._activity_rwsem_acquired_mutex: + self._activity_rwsem_acquired_by_us = True + + # Clear all the NotificationBoxes from upper area + # we don't want people to click on them during the + # the repo update. Kill the completely. + if self._nc is not None: + self._nc.clear_safe() + + self._terminal.reset() + self._release_local_resources() + iface = dbus.Interface( + self._entropy_bus, + dbus_interface=self.DBUS_INTERFACE) + iface.update_repositories(repositories, 1, force) + + def update_repositories(self, repositories, force): + """ + Local method used to start Entropy repositories + update. + """ + if not self._scale_up(): + return + + if self._wc is not None: + self._wc.activate_progress_bar() + self._wc.deactivate_app_box() + + self.emit("repositories-updating", Rigo.WORK_VIEW_STATE) + if const_debug_enabled(): + const_debug_write(__name__, "RigoServiceController: " + "repositories-updating") + + task = ParallelTask(self._update_repositories, + repositories, force) + task.name = "UpdateRepositoriesThread" + task.daemon = True + task.start() + + +class WorkViewController(GObject.Object): + + def __init__(self, rigo_service, work_box): + self._service = rigo_service + self._box = work_box + + def _setup_terminal_menu(self): + """ + Setup TerminalWidget Right Click popup-menu. + """ + self._terminal_menu = Gtk.Menu() + + sall_menu_item = Gtk.ImageMenuItem.new_from_stock( + "gtk-select-all", None) + sall_menu_item.connect("activate", self._on_terminal_select_all) + self._terminal_menu.append(sall_menu_item) + + copy_menu_item = Gtk.ImageMenuItem.new_from_stock( + "gtk-copy", None) + copy_menu_item.connect("activate", self._on_terminal_copy) + self._terminal_menu.append(copy_menu_item) + + reset_menu_item = Gtk.ImageMenuItem.new_from_stock( + "gtk-clear", None) + reset_menu_item.connect("activate", self._on_terminal_reset) + self._terminal_menu.append(reset_menu_item) + + self._terminal_menu.show_all() + + def _setup_terminal_area(self): + """ + Setup TerminalWidget area (including ScrollBar). + """ + hbox = Gtk.HBox() + + self._terminal = TerminalWidget() + self._terminal.connect( + "button-press-event", + self._on_terminal_click) + self._terminal.reset() + hbox.pack_start(self._terminal, True, True, 0) + + scrollbar = Gtk.VScrollbar.new(self._terminal.adjustment) + hbox.pack_start(scrollbar, False, False, 0) + hbox.show_all() + + return hbox + + def _setup_progress_area(self): + """ + Setup Progress Bar area. + """ + self._progress_box = Gtk.VBox() + + progress_align = Gtk.Alignment() + progress_align.set_padding(10, 10, 0, 0) + self._progress_bar = Gtk.ProgressBar() + progress_align.add(self._progress_bar) + self._progress_box.pack_start(progress_align, False, False, 0) + self._progress_box.show_all() + + return self._progress_box + + def setup(self): + """ + Initialize WorkViewController controlled resources. + """ + self._setup_terminal_menu() + + box = self._setup_progress_area() + self._box.pack_start(box, False, False, 0) + + box = self._setup_terminal_area() + self._box.pack_start(box, True, True, 0) + + self._service.set_terminal(self._terminal) + self._service.set_work_controller(self) + + self.deactivate_progress_bar() + self.deactivate_app_box() + + def activate_app_box(self): + """ + Activate the Application Box showing information + about the Application being currently handled. + """ + # FIXME, complete + + def deactivate_app_box(self): + """ + Deactivate the Application Box showing information + about the Application being currently handled. + """ + # FIXME, complete + + def activate_progress_bar(self): + """ + Activate the Progress Bar showing progress information. + """ + self._progress_box.show_all() + + def deactivate_progress_bar(self): + """ + Deactivate the Progress Bar showing progress information. + """ + self._progress_box.hide() + + def set_progress(self, fraction, text=None): + """ + Set Progress Bar progress, progress must be a value between + 0.0 and 1.0. You can also provide a new text for progress at + the same time, the same will be escaped and cleaned out + by the callee. + """ + self._progress_bar.set_fraction(fraction) + if text is not None: + self._progress_bar.set_text(escape_markup(text)) + + def set_progress_text(self, text): + """ + Set Progress Bar text. The same will be escaped and cleaned out by + the callee. + """ + self._progress_bar.set_text(escape_markup(text)) + + def _on_terminal_click(self, widget, event): + """ + Right Click on the TerminalWidget area. + """ + if event.button == 3: + self._terminal_menu.popup( + None, None, None, None, event.button, event.time) + + def _on_terminal_copy(self, widget): + """ + Copy to clipboard Terminal GtkMenuItem clicked. + """ + self._terminal.copy_clipboard() + + def _on_terminal_reset(self, widget): + """ + Reset Terminal GtkMenuItem clicked. + """ + self._terminal.reset() + + def _on_terminal_select_all(self, widget): + """ + Select All Terminal GtkMenuItem clicked. + """ + self._terminal.select_all() + class ApplicationsViewController(GObject.Object): @@ -77,6 +547,11 @@ class ApplicationsViewController(GObject.Object): None, tuple(), ), + # View has been filled + "view-want-change" : (GObject.SignalFlags.RUN_LAST, + None, + (GObject.TYPE_PYOBJECT,), + ), # User logged in to Entropy Web Services "logged-in" : (GObject.SignalFlags.RUN_LAST, None, @@ -89,10 +564,13 @@ class ApplicationsViewController(GObject.Object): ), } - def __init__(self, entropy_client, entropy_ws, icons, nf_box, + def __init__(self, activity_rwsem, entropy_client, entropy_ws, + rigo_service, icons, nf_box, search_entry, store, view): GObject.Object.__init__(self) + self._activity_rwsem = activity_rwsem self._entropy = entropy_client + self._service = rigo_service self._icons = icons self._entropy_ws = entropy_ws self._search_entry = search_entry @@ -134,32 +612,52 @@ class ApplicationsViewController(GObject.Object): def _prepare_for_search(txt): return txt.replace(" ", "-").lower() - matches = [] + ## special keywords hook + if text == "rigo:update": + self._update_repositories_safe() + return + if text == "rigo:vte": + GLib.idle_add(self.emit, "view-want-change", Rigo.WORK_VIEW_STATE) + return + if text == "rigo:output": + GLib.idle_add(self.emit, "view-want-change", Rigo.WORK_VIEW_STATE) + GLib.idle_add(self._service.output_test) + return - # exact match - pkg_matches, rc = self._entropy.atom_match( - text, multi_match = True, - multi_repo = True, mask_filter = False) - matches.extend(pkg_matches) + # Do not execute search if repositories are + # being hold by other write + self._activity_rwsem.reader_acquire() + try: - # atom searching (name and desc) - search_matches = self._entropy.atom_search( - text, - repositories = self._entropy.repositories(), - description = True) + matches = [] - matches.extend([x for x in search_matches if x not in matches]) + # exact match + pkg_matches, rc = self._entropy.atom_match( + text, multi_match = True, + multi_repo = True, mask_filter = False) + matches.extend(pkg_matches) - if not search_matches: + # atom searching (name and desc) search_matches = self._entropy.atom_search( - _prepare_for_search(text), - repositories = self._entropy.repositories()) + text, + repositories = self._entropy.repositories(), + description = True) + matches.extend([x for x in search_matches if x not in matches]) - # we have to decide if to show the treeview in - # the UI thread, to avoid races (and also because we - # have to...) - self.set_many_safe(matches, _from_search=text) + if not search_matches: + search_matches = self._entropy.atom_search( + _prepare_for_search(text), + repositories = self._entropy.repositories()) + matches.extend([x for x in search_matches if x not in matches]) + + # we have to decide if to show the treeview in + # the UI thread, to avoid races (and also because we + # have to...) + self.set_many_safe(matches, _from_search=text) + + finally: + self._activity_rwsem.reader_release() def _setup_search_view(self, items_count, text): """ @@ -238,6 +736,19 @@ class ApplicationsViewController(GObject.Object): self._not_found_search_box = box_align return box_align + def _update_repositories(self): + """ + Spawn Repository Update on RigoDaemon + """ + self._service.update_repositories( + self._entropy.repositories(), True) + + def _update_repositories_safe(self): + """ + Same as _update_repositories() but thread safe. + """ + GLib.idle_add(self._update_repositories) + def setup(self): self._view.set_model(self._store) self._search_entry.connect( @@ -297,10 +808,13 @@ class NotificationViewController(GObject.Object): but also accepts external NotificationBox instances as well. """ - def __init__(self, entropy_client, entropy_ws, avc, notification_box): + def __init__(self, activity_rwsem, entropy_client, entropy_ws, rigo_service, + avc, notification_box): GObject.Object.__init__(self) + self._activity_rwsem = activity_rwsem self._entropy = entropy_client self._entropy_ws = entropy_ws + self._service = rigo_service self._avc = avc self._box = notification_box self._updates = None @@ -334,6 +848,9 @@ class NotificationViewController(GObject.Object): if self._entropy_ws.get(repository_id) is not None: available = True break + if not repositories: + # no repos to check against + available = True if not available: GLib.idle_add(self._notify_connectivity_issues) @@ -348,14 +865,19 @@ class NotificationViewController(GObject.Object): return sorted(updates, key=_key_func) def __calculate_updates(self): - if Repository.are_repositories_old(): - GLib.idle_add(self._notify_old_repositories_safe) - return + self._activity_rwsem.reader_acquire() + try: + if Repository.are_repositories_old(): + GLib.idle_add(self._notify_old_repositories_safe) + return + + updates, removal, fine, spm_fine = \ + self._entropy.calculate_updates() + self._updates = self.__order_updates(updates) + self._security_updates = self._entropy.calculate_security_updates() + finally: + self._activity_rwsem.reader_release() - updates, removal, fine, spm_fine = \ - self._entropy.calculate_updates() - self._updates = self.__order_updates(updates) - self._security_updates = self._entropy.calculate_security_updates() GLib.idle_add(self._notify_updates_safe) def _notify_connectivity_issues(self): @@ -398,6 +920,8 @@ class NotificationViewController(GObject.Object): """ # FIXME, lxnay complete print("On Upgrade Request Received", args) + # FIXME, this is for testing, REMOVE !!!! + self._service.update_repositories(self._entropy.repositories(), True) def _on_update(self, *args): """ @@ -405,6 +929,7 @@ class NotificationViewController(GObject.Object): """ # FIXME, lxnay complete print("On Update Request Received", args) + self._service.update_repositories(self._entropy.repositories(), True) def _on_update_show(self, *args): """ @@ -1355,30 +1880,66 @@ class ApplicationViewController(GObject.Object): class Rigo(Gtk.Application): - class RigoHandler: + class RigoHandler(object): + + def __init__(self, rigo_app): + self._app = rigo_app + + def onDeleteWindow(self, window, event): + # if UI is locked, do not allow to close Rigo + if self._app.is_ui_locked(): + rc = self._app._show_yesno_dialog( + None, + escape_markup(_("Hey hey hey!")), + escape_markup(_("Rigo is working, are you sure?"))) + print rc + return True - def onDeleteWindow(self, *args): while True: try: entropy.tools.kill_threads() - Gtk.main_quit(*args) + Gtk.main_quit((window, event)) except KeyboardInterrupt: continue break # Possible Rigo Application UI States BROWSER_VIEW_STATE, STATIC_VIEW_STATE, \ - APPLICATION_VIEW_STATE = range(3) + APPLICATION_VIEW_STATE, \ + WORK_VIEW_STATE = range(4) def __init__(self): - icons = get_sc_icon_theme(DATA_DIR) + self._current_state_lock = False + self._current_state = Rigo.STATIC_VIEW_STATE + self._state_transactions = { + Rigo.BROWSER_VIEW_STATE: ( + self._enter_browser_state, + self._exit_browser_state), + Rigo.STATIC_VIEW_STATE: ( + self._enter_static_state, + self._exit_static_state), + Rigo.APPLICATION_VIEW_STATE: ( + self._enter_application_state, + self._exit_application_state), + Rigo.WORK_VIEW_STATE: ( + self._enter_work_state, + self._exit_work_state), + } + self._state_mutex = Lock() + icons = get_sc_icon_theme(DATA_DIR) + app_handler = Rigo.RigoHandler(self) + + self._activity_rwsem = ReadersWritersSemaphore() self._entropy = Client() self._entropy_ws = EntropyWebService(self._entropy) + self._service = RigoServiceController( + self, self._activity_rwsem, + self._entropy, self._entropy_ws) self._builder = Gtk.Builder() self._builder.add_from_file(os.path.join(DATA_DIR, "ui/gtk3/rigo.ui")) - self._builder.connect_signals(Rigo.RigoHandler()) + self._builder.connect_signals(app_handler) self._window = self._builder.get_object("rigoWindow") self._window.set_name("rigo-view") self._apps_view = self._builder.get_object("appsViewVbox") @@ -1391,6 +1952,8 @@ class Rigo(Gtk.Application): self._search_entry = self._builder.get_object("searchEntry") self._static_view = self._builder.get_object("staticViewVbox") self._notification = self._builder.get_object("notificationBox") + self._work_view = self._builder.get_object("workViewVbox") + self._work_view.set_name("rigo-view") self._app_view_c = ApplicationViewController( self._entropy, self._entropy_ws, self._builder) @@ -1422,38 +1985,65 @@ class Rigo(Gtk.Application): Gdk.Screen.get_default(), DATA_DIR) - self._current_state = Rigo.STATIC_VIEW_STATE - self._state_transactions = { - Rigo.BROWSER_VIEW_STATE: ( - self._enter_browser_state, - self._exit_browser_state), - Rigo.STATIC_VIEW_STATE: ( - self._enter_static_state, - self._exit_static_state), - Rigo.APPLICATION_VIEW_STATE: ( - self._enter_application_state, - self._exit_application_state), - } - self._state_mutex = Lock() self._avc = ApplicationsViewController( - self._entropy, self._entropy_ws, icons, self._not_found_box, + self._activity_rwsem, + self._entropy, self._entropy_ws, self._service, + icons, self._not_found_box, self._search_entry, self._app_store, self._view) self._avc.connect("view-cleared", self._on_view_cleared) self._avc.connect("view-filled", self._on_view_filled) + self._avc.connect("view-want-change", self._on_view_change) self._nc = NotificationViewController( - self._entropy, self._entropy_ws, + self._activity_rwsem, self._entropy, + self._entropy_ws, self._service, self._avc, self._notification) + self._app_view_c.set_notification_controller(self._nc) self._app_view_c.set_applications_controller(self._avc) + self._service.set_applications_controller(self._avc) + self._service.set_notification_controller(self._nc) + + self._service.connect("repositories-updating", self._on_repo_updating) + self._service.connect("repositories-updated", self._on_repo_updated) + + self._work_view_c = WorkViewController( + self._service, self._work_view) + + def is_ui_locked(self): + """ + Return whether the UI is currently locked. + """ + return self._current_state_lock + + def _on_repo_updating(self, widget, state): + """ + Emitted by RigoServiceController when we're asked to + lock down the UI to a given state. + """ + self._search_entry.set_sensitive(False) + self._change_view_state(state, lock=True) + + def _on_repo_updated(self, widget): + """ + Emitted by RigoServiceController when we're allowed to + let the user mess again with the UI. + """ + with self._state_mutex: + self._current_state_lock = False + self._search_entry.set_sensitive(True) + def _on_view_cleared(self, *args): self._change_view_state(Rigo.STATIC_VIEW_STATE) def _on_view_filled(self, *args): self._change_view_state(Rigo.BROWSER_VIEW_STATE) + def _on_view_change(self, widget, state): + self._change_view_state(state) + def _on_application_show(self, *args): self._change_view_state(Rigo.APPLICATION_VIEW_STATE) @@ -1516,13 +2106,31 @@ class Rigo(Gtk.Application): self._app_view.hide() self._app_view_c.hide() - def _change_view_state(self, state): + def _enter_work_state(self): + """ + Action triggered when UI enters the Work View state (or mode). + Either for Updating Repositories or Installing new Apps. + """ + self._work_view.show() + + def _exit_work_state(self): + """ + Action triggered when UI exits the Work View state (or mode). + """ + self._work_view.hide() + + def _change_view_state(self, state, lock=False): """ Change Rigo Application UI state. You can pass a custom widget that will be shown in case of static view state. """ with self._state_mutex: + if self._current_state_lock: + const_debug_write( + __name__, + "cannot change view state, UI locked") + return False txc = self._state_transactions.get(state) if txc is None: raise AttributeError("wrong view state") @@ -1535,6 +2143,10 @@ class Rigo(Gtk.Application): # enter the new state enter_st() self._current_state = state + if lock: + self._current_state_lock = True + + return True def _change_view_state_safe(self, state): """ @@ -1562,6 +2174,19 @@ class Rigo(Gtk.Application): dlg.run() dlg.destroy() + def _show_yesno_dialog(self, parent, title, message): + """ + Show ugly OK dialog window. + """ + dlg = Gtk.MessageDialog(parent=parent, + type=Gtk.MessageType.INFO, + buttons=GTK_BUTTONS_YES_NO) + dlg.set_markup(message) + dlg.set_title(title) + rc = dlg.run() + dlg.destroy() + return rc + def _permissions_setup(self): """ Check execution privileges and spawn the Rigo UI. @@ -1577,8 +2202,9 @@ class Rigo(Gtk.Application): Gtk.main_quit() return - acquired = entropy.tools.acquire_entropy_locks( - self._entropy, shared=True, max_tries=1) + acquired = not self._entropy.wait_resources( + max_lock_count=1, + shared=True) if not acquired: self._show_ok_dialog( None, @@ -1588,9 +2214,40 @@ class Rigo(Gtk.Application): Gtk.main_quit() return + # check RigoDaemon, don't worry about races between Rigo Clients + # it is fine to have multiple Rigo Clients connected. Mutual + # exclusion is handled via Entropy Resources Lock (which is a file + # based rwsem). + activity = self._service.activity() + if activity != ActivityStates.AVAILABLE: + if activity == ActivityStates.NOT_AVAILABLE: + msg = _("Background Service is currently not available") + elif activity == ActivityStates.UPDATING_REPOSITORIES: + # FIXME, jump to WORK_VIEW and show the progress. + msg = _("Background Service is updating repositories") + elif activity == ActivityStates.INSTALLING_APPLICATION: + # FIXME, jump to WORK_VIEW and show the progress. + msg = _("Background Service is installing Applications") + elif activity == ActivityStates.UPGRADING_SYSTEM: + # FIXME, jump to WORK_VIEW and show the progress. + msg = _("Background Service is updating your system") + elif activity == ActivityStates.INTERNAL_ROUTINES: + msg = _("Background Service is currently busy") + else: + msg = _("Background Service is incompatible with Rigo") + + self._show_ok_dialog( + None, + escape_markup(_("Rigo")), + escape_markup(msg)) + entropy.tools.kill_threads() + Gtk.main_quit() + return + self._app_view_c.setup() self._avc.setup() self._nc.setup() + self._work_view_c.setup() self._window.show() def run(self): @@ -1608,5 +2265,7 @@ class Rigo(Gtk.Application): entropy.tools.kill_threads() if __name__ == "__main__": + import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) app = Rigo() app.run()