class Manager(GObject.GObject): __object_signal__ = (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )) __object_string_signal__ = (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ( GObject.TYPE_PYOBJECT, GObject.TYPE_STRING, )) __gsignals__ = { 'tasks-deleted': __object_signal__, 'task-status-changed': __object_string_signal__, } ############## init ##################################################### def __init__(self, req): GObject.GObject.__init__(self) self.req = req self.config_obj = self.req.get_global_config() self.browser_config = self.config_obj.get_subconfig("browser") self.plugins_config = self.config_obj.get_subconfig("plugins") self.task_config = self.config_obj.get_taskconfig() # Editors self.opened_task = {} # This is the list of tasks that are already # opened in an editor of course it's empty # right now self.browser = None self.__start_browser_hidden = False self.gtk_terminate = False # if true, the gtk main is not started # if true, closing the last window doesn't quit GTG # (GTG lives somewhere else without GUI, e.g. notification area) self.daemon_mode = False # Shared clipboard self.clipboard = clipboard.TaskClipboard(self.req) # Initialize Timer self.config = self.req.get_config('browser') self.timer = Timer(self.config) # Browser (still hidden) self.browser = TaskBrowser(self.req, self) self.__init_plugin_engine() if not self.__start_browser_hidden: self.show_browser() # Deletion UI self.delete_dialog = None # Preferences and Backends windows # Initialize dialogs self.preferences = PreferencesDialog(self.req, self) self.plugins = PluginsDialog(self.config_obj) self.edit_backends_dialog = None # Tag Editor self.tag_editor_dialog = None # DBus DBusTaskWrapper(self.req, self) Log.debug("Manager initialization finished") def __init_plugin_engine(self): self.pengine = PluginEngine(GTG.PLUGIN_DIR) # initializes the plugin api class self.plugin_api = PluginAPI(self.req, self) self.pengine.register_api(self.plugin_api) # checks the conf for user settings try: plugins_enabled = self.plugins_config.get("enabled") except configparser.Error: plugins_enabled = [] for plugin in self.pengine.get_plugins(): plugin.enabled = plugin.module_name in plugins_enabled # initializes and activates each plugin (that is enabled) self.pengine.activate_plugins() ############## Browser ################################################# def open_browser(self): if not self.browser: self.browser = TaskBrowser(self.req, self) # notify user if backup was used backend_dic = self.req.get_all_backends() for backend in backend_dic: if backend.get_name() == "backend_localfile" and \ backend.used_backup(): backend.notify_user_about_backup() Log.debug("Browser is open") # FIXME : the browser should not be the center of the universe. # In fact, we should build a system where view can register themselves # as "stay_alive" views. As long as at least one "stay_alive" view # is registered, gtg keeps running. It quit only when the last # "stay_alive view" is closed (and then unregistered). # Currently, the browser is our only "stay_alive" view. def close_browser(self, sender=None): self.hide_browser() # may take a while to quit self.quit() def hide_browser(self, sender=None): self.browser.hide() def iconify_browser(self, sender=None): self.browser.iconify() def show_browser(self, sender=None): self.browser.show() def is_browser_visible(self, sender=None): return self.browser.is_visible() def get_browser(self): # used by the plugin api to hook in the browser return self.browser def start_browser_hidden(self): self.__start_browser_hidden = True def set_daemon_mode(self, in_daemon_mode): """ Used by notification area plugin to override the behavior: last closed window quits GTG """ self.daemon_mode = in_daemon_mode ################# Task Editor ############################################ def get_opened_editors(self): ''' Returns a dict of task_uid -> TaskEditor, one for each opened editor window ''' return self.opened_task def open_task(self, uid, thisisnew=False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Else, we create a new one. """ t = self.req.get_task(uid) tv = None if uid in self.opened_task: tv = self.opened_task[uid] tv.present() elif t: tv = TaskEditor(requester=self.req, vmanager=self, task=t, taskconfig=self.task_config, thisisnew=thisisnew, clipboard=self.clipboard) tv.present() # registering as opened self.opened_task[uid] = tv # save that we opened this task opened_tasks = self.browser_config.get("opened_tasks") if uid not in opened_tasks: opened_tasks.append(uid) self.browser_config.set("opened_tasks", opened_tasks) return tv def close_task(self, tid): # When an editor is closed, it should de-register itself. if tid in self.opened_task: # the following line has the side effect of removing the # tid key in the opened_task dictionary. editor = self.opened_task[tid] if editor: del self.opened_task[tid] # we have to remove the tid from opened_task first # else, it close_task would be called once again # by editor.close editor.close() opened_tasks = self.browser_config.get("opened_tasks") if tid in opened_tasks: opened_tasks.remove(tid) self.browser_config.set("opened_tasks", opened_tasks) self.check_quit_condition() def check_quit_condition(self): ''' checking if we need to shut down the whole GTG (if no window is open) ''' if not self.daemon_mode and not self.is_browser_visible() and \ not self.opened_task: # no need to live" self.quit() ################ Others dialog ############################################ def open_edit_backends(self, sender=None, backend_id=None): if not self.edit_backends_dialog: self.edit_backends_dialog = BackendsDialog(self.req) self.edit_backends_dialog.activate() if backend_id is not None: self.edit_backends_dialog.show_config_for_backend(backend_id) def configure_backend(self, backend_id): self.open_edit_backends(None, backend_id) def open_preferences(self, config_priv): self.preferences.activate() def configure_plugins(self): self.plugins.activate() def ask_delete_tasks(self, tids): if not self.delete_dialog: self.delete_dialog = DeletionUI(self.req) finallist = self.delete_dialog.delete_tasks(tids) for t in finallist: if t.get_id() in self.opened_task: self.close_task(t.get_id()) GObject.idle_add(self.emit, "tasks-deleted", finallist) return finallist def open_tag_editor(self, tag): if not self.tag_editor_dialog: self.tag_editor_dialog = TagEditor(self.req, self, tag) else: self.tag_editor_dialog.set_tag(tag) self.tag_editor_dialog.show() self.tag_editor_dialog.present() def close_tag_editor(self): self.tag_editor_dialog.hide() ### STATUS ################################################################### def ask_set_task_status(self, task, new_status): ''' Both browser and editor have to use this central method to set task status. It also emits a signal with the task instance as first and the new status as second parameter ''' task.set_status(new_status) GObject.idle_add(self.emit, "task-status-changed", task, new_status) ### URIS ################################################################### def open_uri_list(self, unused, uri_list): ''' Open the Editor windows of the tasks associated with the uris given. Uris are of the form gtg://<taskid> ''' for uri in uri_list: if uri.startswith("gtg://"): self.open_task(uri[6:]) # if no window was opened, we just quit self.check_quit_condition() ### MAIN ################################################################### def main(self, once_thru=False, uri_list=[]): if uri_list: # before opening the requested tasks, we make sure that all of them # are loaded. BackendSignals().connect('default-backend-loaded', self.open_uri_list, uri_list) else: self.open_browser() GObject.threads_init() if not self.gtk_terminate: if once_thru: Gtk.main_iteration() else: Gtk.main() return 0 def quit(self, sender=None): Gtk.main_quit() # save opened tasks and their positions. open_task = [] for otid in list(self.opened_task.keys()): open_task.append(otid) self.opened_task[otid].close() self.browser_config.set("opened_tasks", open_task) # adds the plugin settings to the conf # FIXME: this code is replicated in the preference window. if len(self.pengine.plugins) > 0: self.plugins_config.clear() self.plugins_config.set( "disabled", [p.module_name for p in self.pengine.get_plugins("disabled")], ) self.plugins_config.set( "enabled", [p.module_name for p in self.pengine.get_plugins("enabled")], ) # plugins are deactivated self.pengine.deactivate_plugins()
class Application(Gtk.Application): # Requester req = None # List of opened tasks (task editor windows). Task IDs are keys, # while the editors are their values. open_tasks = {} # The main window (AKA Task Browser) browser = None # Configuration sections config = None config_plugins = None # Shared clipboard clipboard = None # Timer to refresh views and purge tasks timer = None # Plugin Engine instance plugin_engine = None # Dialogs preferences_dialog = None plugins_dialog = None backends_dialog = None delete_task_dialog = None edit_tag_dialog = None def __init__(self, app_id): """Setup Application.""" super().__init__(application_id=app_id, flags=Gio.ApplicationFlags.HANDLES_OPEN) # -------------------------------------------------------------------------- # INIT # -------------------------------------------------------------------------- def do_startup(self): """Callback when primary instance should initialize""" Gtk.Application.do_startup(self) # Register backends datastore = DataStore() for backend_dic in BackendFactory().get_saved_backends_list(): datastore.register_backend(backend_dic) # Save the backends directly to be sure projects.xml is written datastore.save(quit=False) self.req = datastore.get_requester() self.config = self.req.get_config("browser") self.config_plugins = self.req.get_config("plugins") self.clipboard = clipboard.TaskClipboard(self.req) self.timer = Timer(self.config) self.timer.connect('refresh', self.autoclean) self.preferences_dialog = Preferences(self.req, self) self.plugins_dialog = PluginsDialog(self.req) if self.config.get('dark_mode'): self.toggle_darkmode() self.init_style() def do_activate(self): """Callback when launched from the desktop.""" self.init_shared() self.browser.present() log.debug("Application activation finished") def do_open(self, files, n_files, hint): """Callback when opening files/tasks""" self.init_shared() log.debug(f'Received {len(files)} Task URIs') if len(files) != n_files: log.warning( f"Length of files {len(files)} != supposed length {n_files}") for file in files: if file.get_uri_scheme() == 'gtg': uri = file.get_uri() if uri[4:6] != '//': log.info(f"Malformed URI, needs gtg://: {uri}") else: parsed = urllib.parse.urlparse(uri) task_id = parsed.netloc log.debug(f'Opening task {task_id}') self.open_task(task_id) else: log.info(f"Unknown task to open: {file.get_uri()}") log.debug("Application opening finished") def init_shared(self): """ Initialize stuff that can't be done in the startup signal, but in the open or activate signals, otherwise GTK will segfault when creating windows in the startup signal """ if not self.browser: # Prevent multiple inits self.init_browser() self.init_actions() self.init_plugin_engine() self.browser.present() def init_browser(self): # Browser (still hidden) if not self.browser: self.browser = MainWindow(self.req, self) if self.props.application_id == 'org.gnome.GTGDevel': self.browser.get_style_context().add_class('devel') def init_plugin_engine(self): """Setup the plugin engine.""" self.plugin_engine = PluginEngine() plugin_api = PluginAPI(self.req, self) self.plugin_engine.register_api(plugin_api) try: enabled_plugins = self.config_plugins.get("enabled") except configparser.Error: enabled_plugins = [] for plugin in self.plugin_engine.get_plugins(): plugin.enabled = plugin.module_name in enabled_plugins self.plugin_engine.activate_plugins() def init_style(self): """Load the application's CSS file.""" screen = Gdk.Screen.get_default() provider = Gtk.CssProvider() add_provider = Gtk.StyleContext.add_provider_for_screen css_path = os.path.join(CSS_DIR, 'style.css') provider.load_from_path(css_path) add_provider(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def toggle_darkmode(self, state=True): """Use dark mode theme.""" settings = Gtk.Settings.get_default() prefs_css = self.preferences_dialog.window.get_style_context() settings.set_property("gtk-application-prefer-dark-theme", state) # Toggle dark mode for preferences and editors if state: prefs_css.add_class('dark') text_tags.use_dark_mode() else: prefs_css.remove_class('dark') text_tags.use_light_mode() def init_actions(self): """Setup actions.""" action_entries = [ ('quit', lambda a, p: self.quit(), ('app.quit', ['<ctrl>Q'])), ('open_about', self.open_about, None), ('open_plugins', self.open_plugins_manager, None), ('new_task', self.new_task, ('app.new_task', ['<ctrl>N'])), ('new_subtask', self.new_subtask, ('app.new_subtask', ['<ctrl><shift>N'])), ('add_parent', self.add_parent, ('app.add_parent', ['<ctrl><shift>P'])), ('edit_task', self.edit_task, ('app.edit_task', ['<ctrl>E'])), ('mark_as_done', self.mark_as_done, ('app.mark_as_done', ['<ctrl>D'])), ('dismiss', self.dismiss, ('app.dismiss', ['<ctrl>I'])), ('open_backends', self.open_backends_manager, None), ('open_help', self.open_help, ('app.open_help', ['F1'])), ('open_preferences', self.open_preferences, ('app.open_preferences', ['<ctrl>comma'])), ('close', self.close_context, ('app.close', ['Escape'])), ('editor.close', self.close_focused_task, ('app.editor.close', ['<ctrl>w'])), ('editor.show_parent', self.open_parent_task, None), ('editor.delete', self.delete_editor_task, None), ('editor.open_tags_popup', self.open_tags_popup_in_editor, None), ] for action, callback, accel in action_entries: simple_action = Gio.SimpleAction.new(action, None) simple_action.connect('activate', callback) simple_action.set_enabled(True) self.add_action(simple_action) if accel is not None: self.set_accels_for_action(*accel) self.plugins_dialog.dialog.insert_action_group('app', self) # -------------------------------------------------------------------------- # ACTIONS # -------------------------------------------------------------------------- def new_task(self, param=None, action=None): """Callback to add a new task.""" self.browser.on_add_task() def new_subtask(self, param, action): """Callback to add a new subtask.""" try: self.get_active_editor().insert_subtask() except AttributeError: self.browser.on_add_subtask() def add_parent(self, param, action): """Callback to add a parent to a task""" if self.browser.have_same_parent(): self.browser.on_add_parent() def edit_task(self, param, action): """Callback to edit a task.""" self.browser.on_edit_active_task() def mark_as_done(self, param, action): """Callback to mark a task as done.""" try: self.get_active_editor().change_status() except AttributeError: self.browser.on_mark_as_done() def dismiss(self, param, action): """Callback to mark a task as done.""" try: self.get_active_editor().dismiss() except AttributeError: self.browser.on_dismiss_task() def open_help(self, action, param): """Open help callback.""" try: Gtk.show_uri(None, "help:gtg", Gdk.CURRENT_TIME) except GLib.Error: log.error('Could not open help') def open_backends_manager(self, action, param): """Callback to open the backends manager dialog.""" self.open_edit_backends() def open_preferences(self, action, param): """Callback to open the preferences dialog.""" self.preferences_dialog.activate() self.preferences_dialog.window.set_transient_for(self.browser) def open_about(self, action, param): """Callback to open the about dialog.""" self.browser.about.show() def open_plugins_manager(self, action, params): """Callback to open the plugins manager dialog.""" self.plugins_dialog.activate() self.plugins_dialog.dialog.set_transient_for(self.browser) def close_context(self, action, params): """Callback to close based on the focus widget.""" editor = self.get_active_editor() search = self.browser.search_entry.is_focus() if editor: self.close_task(editor.task.get_id()) elif search: self.browser.toggle_search(action, params) def close_focused_task(self, action, params): """Callback to close currently focused task editor.""" editor = self.get_active_editor() if editor: self.close_task(editor.task.get_id()) def delete_editor_task(self, action, params): """Callback to delete the task currently open.""" editor = self.get_active_editor() task = editor.task if task.is_new(): self.close_task(task.get_id()) else: self.delete_tasks([task.get_id()], editor.window) def open_tags_popup_in_editor(self, action, params): """Callback to open the tags popup in the focused task editor.""" editor = self.get_active_editor() editor.open_tags_popover() def open_parent_task(self, action, params): """Callback to open the parent of the currently open task.""" editor = self.get_active_editor() editor.open_parent() # -------------------------------------------------------------------------- # TASKS AUTOCLEANING # -------------------------------------------------------------------------- def purge_old_tasks(self, widget=None): """Remove closed tasks older than N days.""" log.debug("Deleting old tasks") today = Date.today() max_days = self.config.get('autoclean_days') closed_tree = self.req.get_tasks_tree(name='inactive') closed_tasks = [ self.req.get_task(tid) for tid in closed_tree.get_all_nodes() ] to_remove = [ t for t in closed_tasks if (today - t.get_closed_date()).days > max_days ] [ self.req.delete_task(task.get_id()) for task in to_remove if self.req.has_task(task.get_id()) ] def autoclean(self, timer): """Run Automatic cleanup of old tasks.""" if self.config.get('autoclean'): self.purge_old_tasks() # -------------------------------------------------------------------------- # TASK BROWSER API # -------------------------------------------------------------------------- def open_edit_backends(self, sender=None, backend_id=None): """Open the backends dialog.""" self.backends_dialog = BackendsDialog(self.req) self.backends_dialog.dialog.insert_action_group('app', self) self.backends_dialog.activate() if backend_id: self.backends_dialog.show_config_for_backend(backend_id) def delete_tasks(self, tids, window): """Present the delete task confirmation dialog.""" if not self.delete_task_dialog: self.delete_task_dialog = DeletionUI(self.req, window) tasks_to_delete = self.delete_task_dialog.show(tids) [ self.close_task(task.get_id()) for task in tasks_to_delete if task.get_id() in self.open_tasks ] def open_tag_editor(self, tag): """Open Tag editor dialog.""" if not self.edit_tag_dialog: self.edit_tag_dialog = TagEditor(self.req, self, tag) self.edit_tag_dialog.set_transient_for(self.browser) self.edit_tag_dialog.insert_action_group('app', self) else: self.edit_tag_dialog.set_tag(tag) self.edit_tag_dialog.present() def close_tag_editor(self): """Close tag editor dialog.""" self.edit_tag_dialog.hide() def select_tag(self, tag): """Select a tag in the browser.""" self.browser.select_on_sidebar(tag) # -------------------------------------------------------------------------- # TASK EDITOR API # -------------------------------------------------------------------------- def reload_opened_editors(self, task_uid_list=None): """Reloads all the opened editors passed in the list 'task_uid_list'. If 'task_uid_list' is not passed or None, we reload all the opened editors. """ if task_uid_list: [ self.open_tasks[tid].reload_editor() for tid in self.open_tasks if tid in task_uid_list ] else: [task.reload_editor() for task in self.open_tasks] def open_task(self, uid, new=False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Otherwise, we create a new one. """ if uid in self.open_tasks: editor = self.open_tasks[uid] editor.present() else: task = self.req.get_task(uid) editor = None if task: editor = TaskEditor(requester=self.req, app=self, task=task, thisisnew=new, clipboard=self.clipboard) editor.present() self.open_tasks[uid] = editor # Save open tasks to config open_tasks = self.config.get("opened_tasks") if uid not in open_tasks: open_tasks.append(uid) self.config.set("opened_tasks", open_tasks) else: log.error(f'Task {uid} could not be found!') return editor def get_active_editor(self): """Get focused task editor window.""" for editor in self.open_tasks.values(): if editor.window.is_active(): return editor def close_task(self, tid): """Close a task editor window.""" try: editor = self.open_tasks[tid] editor.close() open_tasks = self.config.get("opened_tasks") if tid in open_tasks: open_tasks.remove(tid) self.config.set("opened_tasks", open_tasks) except KeyError: log.debug(f'Tried to close tid {tid} but it is not open') # -------------------------------------------------------------------------- # SHUTDOWN # -------------------------------------------------------------------------- def save_tasks(self): """Save opened tasks and their positions.""" open_task = [] for otid in list(self.open_tasks.keys()): open_task.append(otid) self.open_tasks[otid].close() self.config.set("opened_tasks", open_task) def save_plugin_settings(self): """Save plugin settings to configuration.""" if self.plugin_engine is None: return # Can't save when none has been loaded if self.plugin_engine.plugins: self.config_plugins.set('disabled', [ p.module_name for p in self.plugin_engine.get_plugins('disabled') ]) self.config_plugins.set('enabled', [ p.module_name for p in self.plugin_engine.get_plugins('enabled') ]) self.plugin_engine.deactivate_plugins() def quit(self): """Quit the application.""" # This is needed to avoid warnings when closing the browser # with editor windows open, because of the "win" # group of actions. self.save_tasks() Gtk.Application.quit(self) def do_shutdown(self): """Callback when GTG is closed.""" self.save_plugin_settings() if self.req is not None: # Save data and shutdown datastore backends self.req.save_datastore(quit=True) Gtk.Application.do_shutdown(self) # -------------------------------------------------------------------------- # MISC # -------------------------------------------------------------------------- def set_debug_flag(self, debug): """Set whenever it should activate debug stuff like logging or not""" if debug: log.setLevel(logging.DEBUG) log.debug("Debug output enabled.") else: log.setLevel(logging.INFO)
class Application(Gtk.Application): # Requester req = None # List of Task URIs to open uri_list = None # List of opened tasks (task editor windows). Task IDs are keys, # while the editors are their values. open_tasks = {} # The main window (AKA Task Browser) browser = None # Configuration sections config = None config_plugins = None # Shared clipboard clipboard = None # Timer to refresh views and purge tasks timer = None # Plugin Engine instance plugin_engine = None # Dialogs preferences_dialog = None plugins_dialog = None backends_dialog = None delete_task_dialog = None edit_tag_dialog = None def __init__(self, app_id, debug): """Setup Application.""" super().__init__(application_id=app_id) if debug: log.setLevel(logging.DEBUG) log.debug("Debug output enabled.") else: log.setLevel(logging.INFO) # Register backends datastore = DataStore() [datastore.register_backend(backend_dic) for backend_dic in BackendFactory().get_saved_backends_list()] # Save the backends directly to be sure projects.xml is written datastore.save(quit=False) self.req = datastore.get_requester() self.config = self.req.get_config("browser") self.config_plugins = self.req.get_config("plugins") self.clipboard = clipboard.TaskClipboard(self.req) self.timer = Timer(self.config) self.timer.connect('refresh', self.autoclean) self.preferences_dialog = Preferences(self.req, self) self.plugins_dialog = PluginsDialog(self.req) self.init_style() # -------------------------------------------------------------------------- # INIT # -------------------------------------------------------------------------- def do_activate(self): """Callback when launched from the desktop.""" # Browser (still hidden) if not self.browser: self.browser = MainWindow(self.req, self) if self.props.application_id == 'org.gnome.GTGDevel': self.browser.get_style_context().add_class('devel') self.init_actions() self.init_plugin_engine() self.browser.present() self.open_uri_list() log.debug("Application activation finished") def init_plugin_engine(self): """Setup the plugin engine.""" self.plugin_engine = PluginEngine() plugin_api = PluginAPI(self.req, self) self.plugin_engine.register_api(plugin_api) try: enabled_plugins = self.config_plugins.get("enabled") except configparser.Error: enabled_plugins = [] for plugin in self.plugin_engine.get_plugins(): plugin.enabled = plugin.module_name in enabled_plugins self.plugin_engine.activate_plugins() def init_style(self): """Load the application's CSS file.""" screen = Gdk.Screen.get_default() provider = Gtk.CssProvider() add_provider = Gtk.StyleContext.add_provider_for_screen css_path = os.path.join(CSS_DIR, 'style.css') provider.load_from_path(css_path) add_provider(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def init_actions(self): """Setup actions.""" action_entries = [ ('quit', lambda a, p: self.quit(), ('app.quit', ['<ctrl>Q'])), ('open_about', self.open_about, None), ('open_plugins', self.open_plugins_manager, None), ('new_task', self.new_task, ('app.new_task', ['<ctrl>N'])), ('new_subtask', self.new_subtask, ('app.new_subtask', ['<ctrl><shift>N'])), ('edit_task', self.edit_task, ('app.edit_task', ['<ctrl>E'])), ('mark_as_done', self.mark_as_done, ('app.mark_as_done', ['<ctrl>D'])), ('dismiss', self.dismiss, ('app.dismiss', ['<ctrl>I'])), ('open_backends', self.open_backends_manager, None), ('open_help', self.open_help, ('app.open_help', ['F1'])), ('open_preferences', self.open_preferences, ('app.open_preferences', ['<ctrl>comma'])), ('editor.close', self.close_focused_task, ('app.editor.close', ['Escape', '<ctrl>w'])), ('editor.show_parent', self.open_parent_task, None), ('editor.delete', self.delete_editor_task, None), ('editor.open_tags_popup', self.open_tags_popup_in_editor, None), ] for action, callback, accel in action_entries: simple_action = Gio.SimpleAction.new(action, None) simple_action.connect('activate', callback) simple_action.set_enabled(True) self.add_action(simple_action) if accel is not None: self.set_accels_for_action(*accel) self.plugins_dialog.dialog.insert_action_group('app', self) def open_uri_list(self): """Open the Editor windows of the tasks associated with the uris given. Uris are of the form gtg://<taskid> """ log.debug(f'Received {len(self.uri_list)} Task URIs') for uri in self.uri_list: if uri.startswith('gtg://'): log.debug(f'Opening task {uri[6:]}') self.open_task(uri[6:]) # if no window was opened, we just quit if not self.browser.is_visible() and not self.open_tasks: self.quit() # -------------------------------------------------------------------------- # ACTIONS # -------------------------------------------------------------------------- def new_task(self, param=None, action=None): """Callback to add a new task.""" self.browser.on_add_task() def new_subtask(self, param, action): """Callback to add a new subtask.""" try: self.get_active_editor().insert_subtask() except AttributeError: self.browser.on_add_subtask() def edit_task(self, param, action): """Callback to edit a task.""" self.browser.on_edit_active_task() def mark_as_done(self, param, action): """Callback to mark a task as done.""" try: self.get_active_editor().change_status() except AttributeError: self.browser.on_mark_as_done() def dismiss(self, param, action): """Callback to mark a task as done.""" try: self.get_active_editor().dismiss() except AttributeError: self.browser.on_dismiss_task() def open_help(self, action, param): """Open help callback.""" openurl("help:gtg") def open_backends_manager(self, action, param): """Callback to open the backends manager dialog.""" self.open_edit_backends() def open_preferences(self, action, param): """Callback to open the preferences dialog.""" self.preferences_dialog.activate() self.preferences_dialog.window.set_transient_for(self.browser) def open_about(self, action, param): """Callback to open the about dialog.""" self.browser.about.show() def open_plugins_manager(self, action, params): """Callback to open the plugins manager dialog.""" self.plugins_dialog.activate() self.plugins_dialog.dialog.set_transient_for(self.browser) def close_focused_task(self, action, params): """Callback to close currently focused task editor.""" editor = self.get_active_editor() if editor: self.close_task(editor.task.get_id()) def delete_editor_task(self, action, params): """Callback to delete the task currently open.""" editor = self.get_active_editor() task = editor.task if task.is_new(): self.close_task(task.get_id()) else: self.delete_tasks([task.get_id()], editor.window) def open_tags_popup_in_editor(self, action, params): """Callback to open the tags popup in the focused task editor.""" editor = self.get_active_editor() editor.open_tags_popover() def open_parent_task(self, action, params): """Callback to open the parent of the currently open task.""" editor = self.get_active_editor() editor.open_parent() # -------------------------------------------------------------------------- # TASKS AUTOCLEANING # -------------------------------------------------------------------------- def purge_old_tasks(self, widget=None): """Remove closed tasks older than N days.""" log.debug("Deleting old tasks") today = Date.today() max_days = self.config.get('autoclean_days') closed_tree = self.req.get_tasks_tree(name='inactive') closed_tasks = [self.req.get_task(tid) for tid in closed_tree.get_all_nodes()] to_remove = [t for t in closed_tasks if (today - t.get_closed_date()).days > max_days] [self.req.delete_task(task.get_id()) for task in to_remove if self.req.has_task(task.get_id())] def autoclean(self, timer): """Run Automatic cleanup of old tasks.""" if self.config.get('autoclean'): self.purge_old_tasks() # -------------------------------------------------------------------------- # TASK BROWSER API # -------------------------------------------------------------------------- def open_edit_backends(self, sender=None, backend_id=None): """Open the backends dialog.""" self.backends_dialog = BackendsDialog(self.req) self.backends_dialog.dialog.insert_action_group('app', self) self.backends_dialog.activate() if backend_id: self.backends_dialog.show_config_for_backend(backend_id) def delete_tasks(self, tids, window): """Present the delete task confirmation dialog.""" if not self.delete_task_dialog: self.delete_task_dialog = DeletionUI(self.req, window) tags_to_delete = self.delete_task_dialog.show(tids) [self.close_task(task.get_id()) for task in tags_to_delete if task.get_id() in self.open_tasks] def open_tag_editor(self, tag): """Open Tag editor dialog.""" if not self.edit_tag_dialog: self.edit_tag_dialog = TagEditor(self.req, self, tag) self.edit_tag_dialog.set_transient_for(self.browser) else: self.edit_tag_dialog.set_tag(tag) self.edit_tag_dialog.present() def close_tag_editor(self): """Close tag editor dialog.""" self.edit_tag_dialog.hide() # -------------------------------------------------------------------------- # TASK EDITOR API # -------------------------------------------------------------------------- def reload_opened_editors(self, task_uid_list=None): """Reloads all the opened editors passed in the list 'task_uid_list'. If 'task_uid_list' is not passed or None, we reload all the opened editors. """ if task_uid_list: [self.open_tasks[tid].reload_editor() for tid in self.open_tasks if tid in task_uid_list] else: [task.reload_editor() for task in self.open_tasks] def open_task(self, uid, new=False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Otherwise, we create a new one. """ if uid in self.open_tasks: editor = self.open_tasks[uid] editor.present() else: task = self.req.get_task(uid) editor = None if task: editor = TaskEditor(requester=self.req, app=self, task=task, thisisnew=new, clipboard=self.clipboard) editor.present() self.open_tasks[uid] = editor # Save open tasks to config open_tasks = self.config.get("opened_tasks") if uid not in open_tasks: open_tasks.append(uid) self.config.set("opened_tasks", open_tasks) else: log.error(f'Task {uid} could not be found!') return editor def get_active_editor(self): """Get focused task editor window.""" for editor in self.open_tasks.values(): if editor.window.is_active(): return editor def close_task(self, tid): """Close a task editor window.""" try: editor = self.open_tasks[tid] editor.close() open_tasks = self.config.get("opened_tasks") if tid in open_tasks: open_tasks.remove(tid) self.config.set("opened_tasks", open_tasks) except KeyError: log.debug(f'Tried to close tid {tid} but it is not open') # -------------------------------------------------------------------------- # SHUTDOWN # -------------------------------------------------------------------------- def save_tasks(self): """Save opened tasks and their positions.""" open_task = [] for otid in list(self.open_tasks.keys()): open_task.append(otid) self.open_tasks[otid].close() self.config.set("opened_tasks", open_task) def save_plugin_settings(self): """Save plugin settings to configuration.""" if self.plugin_engine.plugins: self.config_plugins.set( 'disabled', [p.module_name for p in self.plugin_engine.get_plugins('disabled')]) self.config_plugins.set( 'enabled', [p.module_name for p in self.plugin_engine.get_plugins('enabled')]) self.plugin_engine.deactivate_plugins() def quit(self): """Quit the application.""" # This is needed to avoid warnings when closing the browser # with editor windows open, because of the "win" # group of actions. self.save_tasks() Gtk.Application.quit(self) def do_shutdown(self): """Callback when GTG is closed.""" self.save_plugin_settings() # Save data and shutdown datastore backends self.req.save_datastore(quit=True) Gtk.Application.do_shutdown(self)
class PluginsDialog(object): """ Dialog for Plugins configuration """ def __init__(self, requester): self.req = requester self.config = self.req.get_config("plugins") builder = Gtk.Builder() builder.add_from_file(ViewConfig.PLUGINS_UI_FILE) self.dialog = builder.get_object("PluginsDialog") self.dialog.set_title(_("Plugins - %s" % info.NAME)) self.plugin_tree = builder.get_object("PluginTree") self.plugin_configure = builder.get_object("plugin_configure") self.plugin_about = builder.get_object("PluginAboutDialog") self.plugin_depends = builder.get_object('PluginDepends') help.add_help_shortcut(self.dialog, "plugins") self.pengine = PluginEngine() # plugin config initiation if self.pengine.get_plugins(): self.config.set( "disabled", [p.module_name for p in self.pengine.get_plugins("disabled")], ) self.config.set( "enabled", [p.module_name for p in self.pengine.get_plugins("enabled")], ) # see constants PLUGINS_COL_* for column meanings self.plugin_store = Gtk.ListStore(str, bool, str, str, bool) builder.connect_signals({ 'on_plugins_help': self.on_help, 'on_plugins_close': self.on_close, 'on_PluginsDialog_delete_event': self.on_close, 'on_PluginTree_cursor_changed': self.on_plugin_select, 'on_plugin_about': self.on_plugin_about, 'on_plugin_configure': self.on_plugin_configure, 'on_PluginAboutDialog_close': self.on_plugin_about_close, }) def _init_plugin_tree(self): """ Initialize the PluginTree Gtk.TreeView. The format is modelled after the one used in gedit; see http://git.gnome.org/browse/gedit/tree/gedit/gedit-plugin-mapnager.c """ # force creation of the Gtk.ListStore so we can reference it self._refresh_plugin_store() # renderer for the toggle column renderer = Gtk.CellRendererToggle() renderer.set_property('xpad', 6) renderer.connect('toggled', self.on_plugin_toggle) # toggle column column = Gtk.TreeViewColumn(None, renderer, active=PLUGINS_COL_ENABLED, activatable=PLUGINS_COL_ACTIVATABLE, sensitive=PLUGINS_COL_ACTIVATABLE) self.plugin_tree.append_column(column) # plugin name column column = Gtk.TreeViewColumn() column.set_spacing(6) # icon renderer for the plugin name column icon_renderer = Gtk.CellRendererPixbuf() icon_renderer.set_property('stock-size', Gtk.IconSize.SMALL_TOOLBAR) icon_renderer.set_property('xpad', 3) column.pack_start(icon_renderer, False) column.set_cell_data_func(icon_renderer, plugin_icon) # text renderer for the plugin name column name_renderer = Gtk.CellRendererText() name_renderer.set_property('ellipsize', Pango.EllipsizeMode.END) column.pack_start(name_renderer, True) column.set_cell_data_func(name_renderer, plugin_markup, self) self.plugin_tree.append_column(column) # finish setup self.plugin_tree.set_model(self.plugin_store) self.plugin_tree.set_search_column(2) def _refresh_plugin_store(self): """ Refresh status of plugins and put it in a Gtk.ListStore """ self.plugin_store.clear() self.pengine.recheck_plugin_errors(True) for name, plugin in self.pengine.plugins.items(): # activateable if there is no error self.plugin_store.append( (name, plugin.enabled, plugin.full_name, plugin.short_description, not plugin.error)) def activate(self): """ Refresh status of plugins and show the dialog """ if len(self.plugin_tree.get_columns()) == 0: self._init_plugin_tree() else: self._refresh_plugin_store() self.dialog.show_all() def on_close(self, widget, data=None): """ Close the plugins dialog.""" self.dialog.hide() return True @classmethod def on_help(cls, widget): """ Open help for plugins """ help.show_help("plugins") return True def on_plugin_toggle(self, widget, path): """Toggle a plugin enabled/disabled.""" iterator = self.plugin_store.get_iter(path) plugin_id = self.plugin_store.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) plugin.enabled = not self.plugin_store.get_value( iterator, PLUGINS_COL_ENABLED) plugins_enabled = self.config.get("enabled") plugins_disabled = self.config.get("disabled") if plugin.enabled: self.pengine.activate_plugins([plugin]) plugins_enabled.append(plugin.module_name) if plugin.module_name in plugins_disabled: plugins_disabled.remove(plugin.module_name) else: self.pengine.deactivate_plugins([plugin]) plugins_disabled.append(plugin.module_name) if plugin.module_name in plugins_enabled: plugins_enabled.remove(plugin.module_name) self.config.set("enabled", plugins_enabled) self.config.set("disabled", plugins_disabled) self.plugin_store.set_value(iterator, PLUGINS_COL_ENABLED, plugin.enabled) self._update_plugin_configure(plugin) def on_plugin_select(self, plugin_tree): """ Callback when user select/unselect a plugin Update the button "Configure plugin" sensitivity """ model, iterator = plugin_tree.get_selection().get_selected() if iterator is not None: plugin_id = model.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) self._update_plugin_configure(plugin) def _update_plugin_configure(self, plugin): """ Enable the button "Configure Plugin" appropriate. """ configurable = plugin.active and plugin.is_configurable() self.plugin_configure.set_property('sensitive', configurable) def on_plugin_configure(self, widget): """ Show the dialog for plugin configuration """ _, iterator = self.plugin_tree.get_selection().get_selected() if iterator is None: return plugin_id = self.plugin_store.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) plugin.instance.configure_dialog(self.dialog) def on_plugin_about(self, widget): """ Display information about a plugin. """ _, iterator = self.plugin_tree.get_selection().get_selected() if iterator is None: return plugin_id = self.plugin_store.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) # FIXME About plugin dialog looks much more different than # it is in the current trunk # FIXME repair it! # FIXME Author is not usually set and is preserved from # previous plugin... :/ self.plugin_about.set_program_name(plugin.full_name) self.plugin_about.set_version(plugin.version) authors = plugin.authors if isinstance(authors, str): authors = "\n".join(author.strip() for author in authors.split(',')) authors = [ authors, ] self.plugin_about.set_authors(authors) description = plugin.description.replace(r'\n', "\n") self.plugin_about.set_comments(description) self.plugin_depends.set_label(plugin_error_text(plugin)) self.plugin_about.show_all() def on_plugin_about_close(self, widget, data=None): """ Close the PluginAboutDialog. """ self.plugin_about.hide() return True
class Manager(object): ############## init ##################################################### def __init__(self, req): self.req = req self.config_obj = self.req.get_global_config() self.config = self.config_obj.conf_dict self.task_config = self.config_obj.task_conf_dict # Editors self.opened_task = {} # This is the list of tasks that are already # opened in an editor of course it's empty # right now self.browser = None self.__start_browser_hidden = False self.gtk_terminate = False #if true, the gtk main is not started #Shared clipboard self.clipboard = clipboard.TaskClipboard(self.req) #Browser (still hidden) self.browser = TaskBrowser(self.req, self) self.__init_plugin_engine() if not self.__start_browser_hidden: self.show_browser() #Deletion UI self.delete_dialog = None #Preferences and Backends windows # Initialize dialogs self.preferences_dialog = None self.edit_backends_dialog = None #DBus DBusTaskWrapper(self.req, self) Log.debug("Manager initialization finished") def __init_plugin_engine(self): self.pengine = PluginEngine(GTG.PLUGIN_DIR) # initializes the plugin api class self.plugin_api = PluginAPI(self.req, self) self.pengine.register_api(self.plugin_api) # checks the conf for user settings try: plugins_enabled = self.config["plugins"]["enabled"] except KeyError: plugins_enabled = [] for plugin in self.pengine.get_plugins(): plugin.enabled = plugin.module_name in plugins_enabled # initializes and activates each plugin (that is enabled) self.pengine.activate_plugins() ############## Browser ################################################# def open_browser(self): if not self.browser: self.browser = TaskBrowser(self.req, self) Log.debug("Browser is open") #FIXME : the browser should not be the center of the universe. # In fact, we should build a system where view can register themselves # as "stay_alive" views. As long as at least one "stay_alive" view # is registered, gtg keeps running. It quit only when the last # "stay_alive view" is closed (and then unregistered). # Currently, the browser is our only "stay_alive" view. def close_browser(self,sender=None): self.hide_browser() #may take a while to quit self.quit() def hide_browser(self,sender=None): self.browser.hide() def iconify_browser(self,sender=None): self.browser.iconify() def show_browser(self,sender=None): self.browser.show() def is_browser_visible(self,sender=None): return self.browser.is_visible() def get_browser(self): #used by the plugin api to hook in the browser return self.browser def start_browser_hidden(self): self.__start_browser_hidden = True ################# Task Editor ############################################ def get_opened_editors(self): ''' Returns a dict of task_uid -> TaskEditor, one for each opened editor window ''' return self.opened_task def open_task(self, uid,thisisnew = False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Else, we create a new one. """ t = self.req.get_task(uid) tv = None if uid in self.opened_task: tv = self.opened_task[uid] tv.present() elif t: tv = TaskEditor( requester = self.req, \ vmanager = self, \ task = t, \ taskconfig = self.task_config, \ thisisnew = thisisnew,\ clipboard = self.clipboard) #registering as opened self.opened_task[uid] = tv return tv def close_task(self, tid): # When an editor is closed, it should de-register itself. if tid in self.opened_task: #the following line has the side effect of removing the # tid key in the opened_task dictionary. editor = self.opened_task[tid] if editor: del self.opened_task[tid] #we have to remove the tid from opened_task first #else, it close_task would be called once again #by editor.close editor.close() self.check_quit_condition() def check_quit_condition(self): ''' checking if we need to shut down the whole GTG (if no window is open) ''' if not self.is_browser_visible() and not self.opened_task: #no need to live print "AAAAAAAAAAA" self.quit() print self.opened_task ################ Others dialog ############################################ def open_edit_backends(self, sender = None, backend_id = None): if not self.edit_backends_dialog: self.edit_backends_dialog = BackendsDialog(self.req) self.edit_backends_dialog.activate() if backend_id != None: self.edit_backends_dialog.show_config_for_backend(backend_id) def configure_backend(self, backend_id): self.open_edit_backends(None, backend_id) def open_preferences(self, config_priv, sender=None): if not hasattr(self, "preferences"): self.preferences = PreferencesDialog(self.config_obj) self.preferences.activate(config_priv) def ask_delete_tasks(self, tids): if not self.delete_dialog: self.delete_dialog = DeletionUI(self.req) if self.delete_dialog.delete_tasks(tids): for t in tids: if t in self.opened_task: self.close_task(t) ### URIS ################################################################### def open_uri_list(self, unused, uri_list): ''' Open the Editor windows of the tasks associated with the uris given. Uris are of the form gtg://<taskid> ''' print self.req.get_all_tasks_list() for uri in uri_list: if uri.startswith("gtg://"): self.open_task(uri[6:]) #if no window was opened, we just quit self.check_quit_condition() ### MAIN ################################################################### def main(self, once_thru = False, uri_list = []): if uri_list: #before opening the requested tasks, we make sure that all of them #are loaded. BackendSignals().connect('default-backend-loaded', self.open_uri_list, uri_list) else: self.open_browser() gobject.threads_init() if not self.gtk_terminate: if once_thru: gtk.main_iteration() else: gtk.main() return 0 def quit(self,sender=None): gtk.main_quit() #save opened tasks and their positions. open_task = [] for otid in self.opened_task.keys(): open_task.append(otid) self.opened_task[otid].close() self.config["browser"]["opened_tasks"] = open_task # adds the plugin settings to the conf #FIXME: this code is replicated in the preference window. if len(self.pengine.plugins) > 0: self.config["plugins"] = {} self.config["plugins"]["disabled"] = \ [p.module_name for p in self.pengine.get_plugins("disabled")] self.config["plugins"]["enabled"] = \ [p.module_name for p in self.pengine.get_plugins("enabled")] # plugins are deactivated self.pengine.deactivate_plugins()
class Manager(GObject.GObject): __object_signal__ = (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,)) __object_string_signal__ = (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, GObject.TYPE_STRING, )) __gsignals__ = {'tasks-deleted': __object_signal__, 'task-status-changed': __object_string_signal__, } # init ################################################################## def __init__(self, req): GObject.GObject.__init__(self) self.req = req self.config_obj = self.req.get_global_config() self.browser_config = self.config_obj.get_subconfig("browser") self.plugins_config = self.config_obj.get_subconfig("plugins") self.task_config = self.config_obj.get_taskconfig() # Editors # This is the list of tasks that are already opened in an editor # of course it's empty right now self.opened_task = {} self.browser = None self.__start_browser_hidden = False self.gtk_terminate = False # if true, the gtk main is not started # if true, closing the last window doesn't quit GTG # (GTG lives somewhere else without GUI, e.g. notification area) self.daemon_mode = False # Shared clipboard self.clipboard = clipboard.TaskClipboard(self.req) # Initialize Timer self.config = self.req.get_config('browser') self.timer = Timer(self.config) # Browser (still hidden) self.browser = TaskBrowser(self.req, self) self.__init_plugin_engine() if not self.__start_browser_hidden: self.show_browser() # Deletion UI self.delete_dialog = None # Preferences and Backends windows # Initialize dialogs self.preferences = PreferencesDialog(self.req, self) self.plugins = PluginsDialog(self.config_obj) self.edit_backends_dialog = None # Tag Editor self.tag_editor_dialog = None # DBus DBusTaskWrapper(self.req, self) Log.debug("Manager initialization finished") def __init_plugin_engine(self): self.pengine = PluginEngine(GTG.PLUGIN_DIR) # initializes the plugin api class self.plugin_api = PluginAPI(self.req, self) self.pengine.register_api(self.plugin_api) # checks the conf for user settings try: plugins_enabled = self.plugins_config.get("enabled") except configparser.Error: plugins_enabled = [] for plugin in self.pengine.get_plugins(): plugin.enabled = plugin.module_name in plugins_enabled # initializes and activates each plugin (that is enabled) self.pengine.activate_plugins() # Browser ############################################################## def open_browser(self): if not self.browser: self.browser = TaskBrowser(self.req, self) # notify user if backup was used backend_dic = self.req.get_all_backends() for backend in backend_dic: if backend.get_name() == "backend_localfile" and \ backend.used_backup(): backend.notify_user_about_backup() Log.debug("Browser is open") # FIXME : the browser should not be the center of the universe. # In fact, we should build a system where view can register themselves # as "stay_alive" views. As long as at least one "stay_alive" view # is registered, gtg keeps running. It quit only when the last # "stay_alive view" is closed (and then unregistered). # Currently, the browser is our only "stay_alive" view. def close_browser(self, sender=None): self.hide_browser() # may take a while to quit self.quit() def hide_browser(self, sender=None): self.browser.hide() def iconify_browser(self, sender=None): self.browser.iconify() def show_browser(self, sender=None): self.browser.show() def is_browser_visible(self, sender=None): return self.browser.is_visible() def get_browser(self): # used by the plugin api to hook in the browser return self.browser def start_browser_hidden(self): self.__start_browser_hidden = True def set_daemon_mode(self, in_daemon_mode): """ Used by notification area plugin to override the behavior: last closed window quits GTG """ self.daemon_mode = in_daemon_mode # Task Editor ############################################################ def get_opened_editors(self): ''' Returns a dict of task_uid -> TaskEditor, one for each opened editor window ''' return self.opened_task def open_task(self, uid, thisisnew=False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Else, we create a new one. """ t = self.req.get_task(uid) tv = None if uid in self.opened_task: tv = self.opened_task[uid] tv.present() elif t: tv = TaskEditor( requester=self.req, vmanager=self, task=t, taskconfig=self.task_config, thisisnew=thisisnew, clipboard=self.clipboard) tv.present() # registering as opened self.opened_task[uid] = tv # save that we opened this task opened_tasks = self.browser_config.get("opened_tasks") if uid not in opened_tasks: opened_tasks.append(uid) self.browser_config.set("opened_tasks", opened_tasks) return tv def close_task(self, tid): # When an editor is closed, it should de-register itself. if tid in self.opened_task: # the following line has the side effect of removing the # tid key in the opened_task dictionary. editor = self.opened_task[tid] if editor: del self.opened_task[tid] # we have to remove the tid from opened_task first # else, it close_task would be called once again # by editor.close editor.close() opened_tasks = self.browser_config.get("opened_tasks") if tid in opened_tasks: opened_tasks.remove(tid) self.browser_config.set("opened_tasks", opened_tasks) self.check_quit_condition() def check_quit_condition(self): ''' checking if we need to shut down the whole GTG (if no window is open) ''' if not self.daemon_mode and not self.is_browser_visible() and \ not self.opened_task: # no need to live" self.quit() # Others dialog ########################################################### def open_edit_backends(self, sender=None, backend_id=None): if not self.edit_backends_dialog: self.edit_backends_dialog = BackendsDialog(self.req) self.edit_backends_dialog.activate() if backend_id is not None: self.edit_backends_dialog.show_config_for_backend(backend_id) def configure_backend(self, backend_id): self.open_edit_backends(None, backend_id) def open_preferences(self, config_priv): self.preferences.activate() def configure_plugins(self): self.plugins.activate() def ask_delete_tasks(self, tids): if not self.delete_dialog: self.delete_dialog = DeletionUI(self.req) finallist = self.delete_dialog.delete_tasks(tids) for t in finallist: if t.get_id() in self.opened_task: self.close_task(t.get_id()) GObject.idle_add(self.emit, "tasks-deleted", finallist) return finallist def open_tag_editor(self, tag): if not self.tag_editor_dialog: self.tag_editor_dialog = TagEditor(self.req, self, tag) else: self.tag_editor_dialog.set_tag(tag) self.tag_editor_dialog.show() self.tag_editor_dialog.present() def close_tag_editor(self): self.tag_editor_dialog.hide() # STATUS ##################################################################### def ask_set_task_status(self, task, new_status): ''' Both browser and editor have to use this central method to set task status. It also emits a signal with the task instance as first and the new status as second parameter ''' task.set_status(new_status) GObject.idle_add(self.emit, "task-status-changed", task, new_status) # URIS ##################################################################### def open_uri_list(self, unused, uri_list): ''' Open the Editor windows of the tasks associated with the uris given. Uris are of the form gtg://<taskid> ''' for uri in uri_list: if uri.startswith("gtg://"): self.open_task(uri[6:]) # if no window was opened, we just quit self.check_quit_condition() # MAIN ##################################################################### def main(self, once_thru=False, uri_list=[]): if uri_list: # before opening the requested tasks, we make sure that all of them # are loaded. BackendSignals().connect('default-backend-loaded', self.open_uri_list, uri_list) else: self.open_browser() GObject.threads_init() if not self.gtk_terminate: if once_thru: Gtk.main_iteration() else: Gtk.main() return 0 def quit(self, sender=None): Gtk.main_quit() # save opened tasks and their positions. open_task = [] for otid in list(self.opened_task.keys()): open_task.append(otid) self.opened_task[otid].close() self.browser_config.set("opened_tasks", open_task) # adds the plugin settings to the conf # FIXME: this code is replicated in the preference window. if len(self.pengine.plugins) > 0: self.plugins_config.clear() self.plugins_config.set( "disabled", [p.module_name for p in self.pengine.get_plugins("disabled")], ) self.plugins_config.set( "enabled", [p.module_name for p in self.pengine.get_plugins("enabled")], ) # plugins are deactivated self.pengine.deactivate_plugins()
class PluginsDialog: """ Dialog for Plugins configuration """ def __init__(self, config_obj): self.config_obj = config_obj self.config = self.config_obj.get_subconfig("plugins") builder = Gtk.Builder() builder.add_from_file(ViewConfig.PLUGINS_UI_FILE) self.dialog = builder.get_object("PluginsDialog") self.dialog.set_title(_("Plugins - %s" % info.NAME)) self.plugin_tree = builder.get_object("PluginTree") self.plugin_configure = builder.get_object("plugin_configure") self.plugin_about = builder.get_object("PluginAboutDialog") self.plugin_depends = builder.get_object('PluginDepends') help.add_help_shortcut(self.dialog, "plugins") self.pengine = PluginEngine() # plugin config initiation if self.pengine.get_plugins(): self.config.set( "disabled", [p.module_name for p in self.pengine.get_plugins("disabled")], ) self.config.set( "enabled", [p.module_name for p in self.pengine.get_plugins("enabled")], ) # see constants PLUGINS_COL_* for column meanings self.plugin_store = Gtk.ListStore(str, bool, str, str, bool) builder.connect_signals({ 'on_plugins_help': self.on_help, 'on_plugins_close': self.on_close, 'on_PluginsDialog_delete_event': self.on_close, 'on_PluginTree_cursor_changed': self.on_plugin_select, 'on_plugin_about': self.on_plugin_about, 'on_plugin_configure': self.on_plugin_configure, 'on_PluginAboutDialog_close': self.on_plugin_about_close, }) def _init_plugin_tree(self): """ Initialize the PluginTree Gtk.TreeView. The format is modelled after the one used in gedit; see http://git.gnome.org/browse/gedit/tree/gedit/gedit-plugin-mapnager.c """ # force creation of the Gtk.ListStore so we can reference it self._refresh_plugin_store() # renderer for the toggle column renderer = Gtk.CellRendererToggle() renderer.set_property('xpad', 6) renderer.connect('toggled', self.on_plugin_toggle) # toggle column column = Gtk.TreeViewColumn(None, renderer, active=PLUGINS_COL_ENABLED, activatable=PLUGINS_COL_ACTIVATABLE, sensitive=PLUGINS_COL_ACTIVATABLE) self.plugin_tree.append_column(column) # plugin name column column = Gtk.TreeViewColumn() column.set_spacing(6) # icon renderer for the plugin name column icon_renderer = Gtk.CellRendererPixbuf() icon_renderer.set_property('stock-size', Gtk.IconSize.SMALL_TOOLBAR) icon_renderer.set_property('xpad', 3) column.pack_start(icon_renderer, False) column.set_cell_data_func(icon_renderer, plugin_icon) # text renderer for the plugin name column name_renderer = Gtk.CellRendererText() name_renderer.set_property('ellipsize', Pango.EllipsizeMode.END) column.pack_start(name_renderer, True) column.set_cell_data_func(name_renderer, plugin_markup, self) self.plugin_tree.append_column(column) # finish setup self.plugin_tree.set_model(self.plugin_store) self.plugin_tree.set_search_column(2) def _refresh_plugin_store(self): """ Refresh status of plugins and put it in a Gtk.ListStore """ self.plugin_store.clear() self.pengine.recheck_plugin_errors(True) for name, plugin in self.pengine.plugins.items(): # activateable if there is no error self.plugin_store.append((name, plugin.enabled, plugin.full_name, plugin.short_description, not plugin.error)) def activate(self): """ Refresh status of plugins and show the dialog """ if len(self.plugin_tree.get_columns()) == 0: self._init_plugin_tree() else: self._refresh_plugin_store() self.dialog.show_all() def on_close(self, widget, data=None): """ Close the plugins dialog.""" self.dialog.hide() return True @classmethod def on_help(cls, widget): """ Open help for plugins """ help.show_help("plugins") return True def on_plugin_toggle(self, widget, path): """Toggle a plugin enabled/disabled.""" iterator = self.plugin_store.get_iter(path) plugin_id = self.plugin_store.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) plugin.enabled = not self.plugin_store.get_value(iterator, PLUGINS_COL_ENABLED) plugins_enabled = self.config.get("enabled") plugins_disabled = self.config.get("disabled") if plugin.enabled: self.pengine.activate_plugins([plugin]) plugins_enabled.append(plugin.module_name) if plugin.module_name in plugins_disabled: plugins_disabled.remove(plugin.module_name) else: self.pengine.deactivate_plugins([plugin]) plugins_disabled.append(plugin.module_name) if plugin.module_name in plugins_enabled: plugins_enabled.remove(plugin.module_name) self.config.set("enabled", plugins_enabled) self.config.set("disabled", plugins_disabled) self.plugin_store.set_value(iterator, PLUGINS_COL_ENABLED, plugin.enabled) self._update_plugin_configure(plugin) self.config_obj.save() def on_plugin_select(self, plugin_tree): """ Callback when user select/unselect a plugin Update the button "Configure plugin" sensitivity """ model, iterator = plugin_tree.get_selection().get_selected() if iterator is not None: plugin_id = model.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) self._update_plugin_configure(plugin) def _update_plugin_configure(self, plugin): """ Enable the button "Configure Plugin" appropriate. """ configurable = plugin.active and plugin.is_configurable() self.plugin_configure.set_property('sensitive', configurable) def on_plugin_configure(self, widget): """ Show the dialog for plugin configuration """ _, iterator = self.plugin_tree.get_selection().get_selected() if iterator is None: return plugin_id = self.plugin_store.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) plugin.instance.configure_dialog(self.dialog) def on_plugin_about(self, widget): """ Display information about a plugin. """ _, iterator = self.plugin_tree.get_selection().get_selected() if iterator is None: return plugin_id = self.plugin_store.get_value(iterator, PLUGINS_COL_ID) plugin = self.pengine.get_plugin(plugin_id) #FIXME About plugin dialog looks much more different than #it is in the current trunk #FIXME repair it! #FIXME Author is not usually set and is preserved from #previous plugin... :/ self.plugin_about.set_program_name(plugin.full_name) self.plugin_about.set_version(plugin.version) authors = plugin.authors if isinstance(authors, str): authors = "\n".join(author.strip() for author in authors.split(',')) authors = [authors, ] self.plugin_about.set_authors(authors) description = plugin.description.replace(r'\n', "\n") self.plugin_about.set_comments(description) self.plugin_depends.set_label(plugin_error_text(plugin)) self.plugin_about.show_all() def on_plugin_about_close(self, widget, data=None): """ Close the PluginAboutDialog. """ self.plugin_about.hide() return True
class Manager(): # init ################################################################## def __init__(self, req): self.req = req self.browser_config = self.req.get_config("browser") self.plugins_config = self.req.get_config("plugins") # Editors # This is the list of tasks that are already opened in an editor # of course it's empty right now self.opened_task = {} self.browser = None self.__start_browser_hidden = False self.gtk_terminate = False # if true, the gtk main is not started # Shared clipboard self.clipboard = clipboard.TaskClipboard(self.req) # Initialize Timer self.config = self.req.get_config('browser') self.timer = Timer(self.config) self.timer.connect('refresh', self.autoclean) # Load custom css self.__init_css() # Browser (still hidden) self.browser = TaskBrowser(self.req, self) self.__init_plugin_engine() if not self.__start_browser_hidden: self.show_browser() # Deletion UI self.delete_dialog = None # Preferences and Backends windows # Initialize dialogs self.preferences = Preferences(self.req, self) self.plugins = PluginsDialog(self.req) self.edit_backends_dialog = None # Tag Editor self.tag_editor_dialog = None # DBus DBusTaskWrapper(self.req, self) log.debug("Manager initialization finished") def __init_plugin_engine(self): self.pengine = PluginEngine() # initializes the plugin api class self.plugin_api = PluginAPI(self.req, self) self.pengine.register_api(self.plugin_api) # checks the conf for user settings try: plugins_enabled = self.plugins_config.get("enabled") except configparser.Error: plugins_enabled = [] for plugin in self.pengine.get_plugins(): plugin.enabled = plugin.module_name in plugins_enabled # initializes and activates each plugin (that is enabled) self.pengine.activate_plugins() def __init_css(self): """Load the application's CSS file.""" screen = Gdk.Screen.get_default() provider = Gtk.CssProvider() css_path = os.path.join(CSS_DIR, 'style.css') provider.load_from_path(css_path) Gtk.StyleContext.add_provider_for_screen( screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) # Browser ############################################################## def open_browser(self): if not self.browser: self.browser = TaskBrowser(self.req, self) # notify user if backup was used backend_dic = self.req.get_all_backends() for backend in backend_dic: if backend.get_name() == "backend_localfile" and \ backend.used_backup(): backend.notify_user_about_backup() log.debug("Browser is open") # FIXME : the browser should not be the center of the universe. # In fact, we should build a system where view can register themselves # as "stay_alive" views. As long as at least one "stay_alive" view # is registered, gtg keeps running. It quit only when the last # "stay_alive view" is closed (and then unregistered). # Currently, the browser is our only "stay_alive" view. def close_browser(self, sender=None): self.hide_browser() # may take a while to quit self.quit() def hide_browser(self, sender=None): self.browser.hide() def iconify_browser(self, sender=None): self.browser.iconify() def show_browser(self, sender=None): self.browser.show() def is_browser_visible(self, sender=None): return self.browser.is_visible() def get_browser(self): # used by the plugin api to hook in the browser return self.browser def start_browser_hidden(self): self.__start_browser_hidden = True def purge_old_tasks(self, widget=None): log.debug("Deleting old tasks") today = Date.today() max_days = self.config.get('autoclean_days') closed_tree = self.req.get_tasks_tree(name='inactive') closed_tasks = [ self.req.get_task(tid) for tid in closed_tree.get_all_nodes() ] to_remove = [ t for t in closed_tasks if (today - t.get_closed_date()).days > max_days ] [ self.req.delete_task(task.get_id()) for task in to_remove if self.req.has_task(task.get_id()) ] def autoclean(self, timer): """Run Automatic cleanup of old tasks.""" if self.config.get('autoclean'): self.purge_old_tasks() # Task Editor ############################################################ def get_opened_editors(self): """ Returns a dict of task_uid -> TaskEditor, one for each opened editor window """ return self.opened_task def reload_opened_editors(self, task_uid_list=None): """Reloads all the opened editors passed in the list 'task_uid_list'. If 'task_uid_list' is not passed or None, we reload all the opened editors. Else, we reload the editors of tasks in 'task_uid_list' only. """ opened_editors = self.get_opened_editors() for t in opened_editors: if not task_uid_list or t in task_uid_list: opened_editors[t].reload_editor() def open_task(self, uid, thisisnew=False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Else, we create a new one. """ t = self.req.get_task(uid) tv = None if uid in self.opened_task: tv = self.opened_task[uid] tv.present() elif t: tv = TaskEditor(requester=self.req, vmanager=self, task=t, thisisnew=thisisnew, clipboard=self.clipboard) tv.present() # registering as opened self.opened_task[uid] = tv # save that we opened this task opened_tasks = self.browser_config.get("opened_tasks") if uid not in opened_tasks: opened_tasks.append(uid) self.browser_config.set("opened_tasks", opened_tasks) return tv def close_task(self, tid): # When an editor is closed, it should de-register itself. if tid in self.opened_task: # the following line has the side effect of removing the # tid key in the opened_task dictionary. editor = self.opened_task[tid] if editor: del self.opened_task[tid] # we have to remove the tid from opened_task first # else, it close_task would be called once again # by editor.close editor.close() opened_tasks = self.browser_config.get("opened_tasks") if tid in opened_tasks: opened_tasks.remove(tid) self.browser_config.set("opened_tasks", opened_tasks) self.check_quit_condition() def check_quit_condition(self): """ checking if we need to shut down the whole GTG (if no window is open) """ if not self.is_browser_visible() and not self.opened_task: self.quit() # Others dialog ########################################################### def open_edit_backends(self, sender=None, backend_id=None): if not self.edit_backends_dialog: self.edit_backends_dialog = BackendsDialog(self.req) self.edit_backends_dialog.activate() if backend_id is not None: self.edit_backends_dialog.show_config_for_backend(backend_id) def configure_backend(self, backend_id): self.open_edit_backends(None, backend_id) def open_preferences(self, config_priv): self.preferences.activate() def configure_plugins(self): self.plugins.activate() def ask_delete_tasks(self, tids, window): if not self.delete_dialog: self.delete_dialog = DeletionUI(self.req, window) finallist = self.delete_dialog.show(tids) for t in finallist: if t.get_id() in self.opened_task: self.close_task(t.get_id()) def open_tag_editor(self, tag): if not self.tag_editor_dialog: self.tag_editor_dialog = TagEditor(self.req, self, tag) else: self.tag_editor_dialog.set_tag(tag) self.tag_editor_dialog.show() self.tag_editor_dialog.present() def close_tag_editor(self): self.tag_editor_dialog.hide() # URIS ##################################################################### def open_uri_list(self, unused, uri_list): """ Open the Editor windows of the tasks associated with the uris given. Uris are of the form gtg://<taskid> """ for uri in uri_list: if uri.startswith("gtg://"): self.open_task(uri[6:]) # if no window was opened, we just quit self.check_quit_condition() # MAIN ##################################################################### def main(self, once_thru=False, uri_list=[]): if uri_list: # before opening the requested tasks, we make sure that all of them # are loaded. BackendSignals().connect('default-backend-loaded', self.open_uri_list, uri_list) else: self.open_browser() GObject.threads_init() if not self.gtk_terminate: if once_thru: Gtk.main_iteration() else: Gtk.main() return 0 def quit(self, sender=None): Gtk.main_quit() # save opened tasks and their positions. open_task = [] for otid in list(self.opened_task.keys()): open_task.append(otid) self.opened_task[otid].close() self.browser_config.set("opened_tasks", open_task) # adds the plugin settings to the conf # FIXME: this code is replicated in the preference window. if len(self.pengine.plugins) > 0: self.plugins_config.set( "disabled", [p.module_name for p in self.pengine.get_plugins("disabled")], ) self.plugins_config.set( "enabled", [p.module_name for p in self.pengine.get_plugins("enabled")], ) # plugins are deactivated self.pengine.deactivate_plugins()
class Application(Gtk.Application): ds: Datastore2 = Datastore2() """Datastore loaded with the default data file""" # Requester req = None # List of opened tasks (task editor windows). Task IDs are keys, # while the editors are their values. open_tasks = {} # The main window (AKA Task Browser) browser = None # Configuration sections config = None config_plugins = None # Shared clipboard clipboard = None # Timer to refresh views and purge tasks timer = None # Plugin Engine instance plugin_engine = None # Dialogs preferences_dialog = None plugins_dialog = None backends_dialog = None delete_task_dialog = None edit_tag_dialog = None def __init__(self, app_id): """Setup Application.""" self._exception = None """Exception that occurred in startup, None otherwise""" self._exception_dialog_timeout_id = None """ Gio Source ID of an timer used to automatically kill GTG on startup error. """ super().__init__(application_id=app_id, flags=Gio.ApplicationFlags.HANDLES_OPEN) self.set_option_context_parameter_string("[gtg://TASK-ID…]") # -------------------------------------------------------------------------- # INIT # -------------------------------------------------------------------------- def do_startup(self): """Callback when primary instance should initialize""" try: Gtk.Application.do_startup(self) Gtk.Window.set_default_icon_name(self.props.application_id) # Load default file data_file = os.path.join(DATA_DIR, 'gtg_data.xml') self.ds.find_and_load_file(data_file) # TODO: Remove this once the new core is stable self.ds.data_path = os.path.join(DATA_DIR, 'gtg_data2.xml') # Register backends datastore = DataStore() for backend_dic in BackendFactory().get_saved_backends_list(): datastore.register_backend(backend_dic) # Save the backends directly to be sure projects.xml is written datastore.save(quit=False) self.req = datastore.get_requester() self.config = self.req.get_config("browser") self.config_plugins = self.req.get_config("plugins") self.clipboard = clipboard.TaskClipboard(self.req) self.timer = Timer(self.config) self.timer.connect('refresh', self.autoclean) self.preferences_dialog = Preferences(self.req, self) self.plugins_dialog = PluginsDialog(self.req) if self.config.get('dark_mode'): self.toggle_darkmode() self.init_style() except Exception as e: self._exception = e log.exception("Exception during startup") self._exception_dialog_timeout_id = GLib.timeout_add( # priority is a kwarg for some reason not reflected in the docs 5000, self._startup_exception_timeout, None) # Don't re-raise to not trigger the global exception hook def do_activate(self): """Callback when launched from the desktop.""" if self._check_exception(): return try: self.init_shared() self.browser.present() log.debug("Application activation finished") except Exception as e: log.exception("Exception during activation") dialog = do_error_dialog(self._exception, "Activation", ignorable=False) dialog.set_application(self) # Keep application alive to show it def do_open(self, files, n_files, hint): """Callback when opening files/tasks""" if self._check_exception(): return try: self.init_shared() len_files = len(files) log.debug("Received %d Task URIs", len_files) if len_files != n_files: log.warning("Length of files %d != supposed length %d", len_files, n_files) for file in files: if file.get_uri_scheme() == 'gtg': uri = file.get_uri() if uri[4:6] != '//': log.info("Malformed URI, needs gtg://:%s", uri) else: parsed = urllib.parse.urlparse(uri) task_id = parsed.netloc log.debug("Opening task %s", task_id) self.open_task(task_id) else: log.info("Unknown task to open: %s", file.get_uri()) log.debug("Application opening finished") except Exception as e: log.exception("Exception during opening") dialog = do_error_dialog(self._exception, "Opening", ignorable=False) dialog.set_application(self) # Keep application alive to show it def _check_exception(self) -> bool: """ Checks whenever an error occured before at startup, and shows an dialog. Returns True whenever such error occurred, False otherwise. """ if self._exception is not None: GLib.Source.remove(self._exception_dialog_timeout_id) self._exception_dialog_timeout_id = None dialog = do_error_dialog(self._exception, "Startup", ignorable=False) dialog.set_application(self) # Keep application alive, for now return self._exception is not None def _startup_exception_timeout(self, user_data): """ Called when an exception in startup occurred, but didn't go over activation/opening a "file" within a specified amount of time. This can be caused by for example trying to call an DBus service, get an error during startup. It also means GTG was started in the background, so showing the user suddenly an error message isn't great, and thus this will just exit the application. Since this is an error case, the normal shutdown procedure isn't used but rather a python exit. """ log.info("Exiting because of startup exception timeout") GLib.Source.remove(self._exception_dialog_timeout_id) self._exception_dialog_timeout_id = None sys.exit(1) def init_shared(self): """ Initialize stuff that can't be done in the startup signal, but in the open or activate signals, otherwise GTK will segfault when creating windows in the startup signal """ if not self.browser: # Prevent multiple inits self.init_browser() self.init_actions() self.init_plugin_engine() self.browser.present() def init_browser(self): # Browser (still hidden) if not self.browser: self.browser = MainWindow(self.req, self) if self.props.application_id == 'org.gnome.GTGDevel': self.browser.get_style_context().add_class('devel') def init_plugin_engine(self): """Setup the plugin engine.""" self.plugin_engine = PluginEngine() plugin_api = PluginAPI(self.req, self) self.plugin_engine.register_api(plugin_api) try: enabled_plugins = self.config_plugins.get("enabled") except configparser.Error: enabled_plugins = [] for plugin in self.plugin_engine.get_plugins(): plugin.enabled = plugin.module_name in enabled_plugins self.plugin_engine.activate_plugins() def init_style(self): """Load the application's CSS file.""" screen = Gdk.Screen.get_default() provider = Gtk.CssProvider() add_provider = Gtk.StyleContext.add_provider_for_screen css_path = os.path.join(CSS_DIR, 'style.css') provider.load_from_path(css_path) add_provider(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def toggle_darkmode(self, state=True): """Use dark mode theme.""" settings = Gtk.Settings.get_default() prefs_css = self.preferences_dialog.window.get_style_context() settings.set_property("gtk-application-prefer-dark-theme", state) # Toggle dark mode for preferences and editors if state: prefs_css.add_class('dark') text_tags.use_dark_mode() else: prefs_css.remove_class('dark') text_tags.use_light_mode() def init_actions(self): """Setup actions.""" action_entries = [ ('quit', lambda a, p: self.quit(), ('app.quit', ['<ctrl>Q'])), ('open_about', self.open_about, None), ('open_plugins', self.open_plugins_manager, None), ('new_task', self.new_task, ('app.new_task', ['<ctrl>N'])), ('new_subtask', self.new_subtask, ('app.new_subtask', ['<ctrl><shift>N'])), ('add_parent', self.add_parent, ('app.add_parent', ['<ctrl><shift>P'])), ('edit_task', self.edit_task, ('app.edit_task', ['<ctrl>E'])), ('mark_as_done', self.mark_as_done, ('app.mark_as_done', ['<ctrl>D'])), ('dismiss', self.dismiss, ('app.dismiss', ['<ctrl><shift>D'])), ('reopen', self.reopen, ('app.reopen', ['<ctrl>O'])), ('open_backends', self.open_backends_manager, None), ('open_help', self.open_help, ('app.open_help', ['F1'])), ('open_preferences', self.open_preferences, ('app.open_preferences', ['<ctrl>comma'])), ('close', self.close_context, ('app.close', ['Escape'])), ('editor.close', self.close_focused_task, ('app.editor.close', ['<ctrl>w'])), ('editor.show_parent', self.open_parent_task, None), ('editor.delete', self.delete_editor_task, None), ('editor.open_tags_popup', self.open_tags_popup_in_editor, None), ] for action, callback, accel in action_entries: simple_action = Gio.SimpleAction.new(action, None) simple_action.connect('activate', callback) simple_action.set_enabled(True) self.add_action(simple_action) if accel is not None: self.set_accels_for_action(*accel) self.plugins_dialog.dialog.insert_action_group('app', self) # -------------------------------------------------------------------------- # ACTIONS # -------------------------------------------------------------------------- def new_task(self, param=None, action=None): """Callback to add a new task.""" self.browser.on_add_task() def new_subtask(self, param, action): """Callback to add a new subtask.""" try: self.get_active_editor().insert_subtask() except AttributeError: self.browser.on_add_subtask() def add_parent(self, param, action): """Callback to add a parent to a task""" try: if self.browser.have_same_parent(): self.browser.on_add_parent() # When no task has been selected except IndexError: return def edit_task(self, param, action): """Callback to edit a task.""" self.browser.on_edit_active_task() def mark_as_done(self, param, action): """Callback to mark a task as done.""" try: self.get_active_editor().change_status() except AttributeError: self.browser.on_mark_as_done() def dismiss(self, param, action): """Callback to mark a task as done.""" try: self.get_active_editor().toggle_dismiss() except AttributeError: self.browser.on_dismiss_task() def reopen(self, param, action): """Callback to mark task as open.""" try: self.get_active_editor().reopen() except AttributeError: self.browser.on_reopen_task() def open_help(self, action, param): """Open help callback.""" try: Gtk.show_uri(None, "help:gtg", Gdk.CURRENT_TIME) except GLib.Error: log.error('Could not open help') def open_backends_manager(self, action, param): """Callback to open the backends manager dialog.""" self.open_edit_backends() def open_preferences(self, action, param): """Callback to open the preferences dialog.""" self.preferences_dialog.activate() self.preferences_dialog.window.set_transient_for(self.browser) def open_about(self, action, param): """Callback to open the about dialog.""" self.browser.about.show() def open_plugins_manager(self, action, params): """Callback to open the plugins manager dialog.""" self.plugins_dialog.activate() self.plugins_dialog.dialog.set_transient_for(self.browser) def close_context(self, action, params): """Callback to close based on the focus widget.""" editor = self.get_active_editor() search = self.browser.search_entry.is_focus() if editor: self.close_task(editor.task.get_id()) elif search: self.browser.toggle_search(action, params) def close_focused_task(self, action, params): """Callback to close currently focused task editor.""" editor = self.get_active_editor() if editor: self.close_task(editor.task.get_id()) def delete_editor_task(self, action, params): """Callback to delete the task currently open.""" editor = self.get_active_editor() task = editor.task if task.is_new(): self.close_task(task.get_id()) else: self.delete_tasks([task.get_id()], editor.window) def open_tags_popup_in_editor(self, action, params): """Callback to open the tags popup in the focused task editor.""" editor = self.get_active_editor() editor.open_tags_popover() def open_parent_task(self, action, params): """Callback to open the parent of the currently open task.""" editor = self.get_active_editor() editor.open_parent() # -------------------------------------------------------------------------- # TASKS AUTOCLEANING # -------------------------------------------------------------------------- def purge_old_tasks(self, widget=None): """Remove closed tasks older than N days.""" log.debug("Deleting old tasks") today = Date.today() max_days = self.config.get('autoclean_days') closed_tree = self.req.get_tasks_tree(name='inactive') closed_tasks = [self.req.get_task(tid) for tid in closed_tree.get_all_nodes()] to_remove = [t for t in closed_tasks if (today - t.get_closed_date()).days > max_days] [self.req.delete_task(task.get_id()) for task in to_remove if self.req.has_task(task.get_id())] def autoclean(self, timer): """Run Automatic cleanup of old tasks.""" if self.config.get('autoclean'): self.purge_old_tasks() # -------------------------------------------------------------------------- # TASK BROWSER API # -------------------------------------------------------------------------- def open_edit_backends(self, sender=None, backend_id=None): """Open the backends dialog.""" self.backends_dialog = BackendsDialog(self.req) self.backends_dialog.dialog.insert_action_group('app', self) self.backends_dialog.activate() if backend_id: self.backends_dialog.show_config_for_backend(backend_id) def delete_tasks(self, tids, window): """Present the delete task confirmation dialog.""" if not self.delete_task_dialog: self.delete_task_dialog = DeletionUI(self.req, window) tasks_to_delete = self.delete_task_dialog.show(tids) [self.close_task(task.get_id()) for task in tasks_to_delete if task.get_id() in self.open_tasks] def open_tag_editor(self, tag): """Open Tag editor dialog.""" self.edit_tag_dialog = TagEditor(self.req, self, tag) self.edit_tag_dialog.set_transient_for(self.browser) self.edit_tag_dialog.insert_action_group('app', self) def close_tag_editor(self): """Close tag editor dialog.""" self.edit_tag_dialog = None def select_tag(self, tag): """Select a tag in the browser.""" self.browser.select_on_sidebar(tag) # -------------------------------------------------------------------------- # TASK EDITOR API # -------------------------------------------------------------------------- def reload_opened_editors(self, task_uid_list=None): """Reloads all the opened editors passed in the list 'task_uid_list'. If 'task_uid_list' is not passed or None, we reload all the opened editors. """ if task_uid_list: [self.open_tasks[tid].reload_editor() for tid in self.open_tasks if tid in task_uid_list] else: [task.reload_editor() for task in self.open_tasks] def open_task(self, uid, new=False): """Open the task identified by 'uid'. If a Task editor is already opened for a given task, we present it. Otherwise, we create a new one. """ if uid in self.open_tasks: editor = self.open_tasks[uid] editor.present() else: task = self.req.get_task(uid) editor = None if task: editor = TaskEditor(requester=self.req, app=self, task=task, thisisnew=new, clipboard=self.clipboard) editor.present() self.open_tasks[uid] = editor # Save open tasks to config open_tasks = self.config.get("opened_tasks") if uid not in open_tasks: open_tasks.append(uid) self.config.set("opened_tasks", open_tasks) else: log.error('Task %s could not be found!', uid) return editor def get_active_editor(self): """Get focused task editor window.""" for editor in self.open_tasks.values(): if editor.window.is_active(): return editor def close_task(self, tid): """Close a task editor window.""" try: editor = self.open_tasks[tid] editor.close() open_tasks = self.config.get("opened_tasks") if tid in open_tasks: open_tasks.remove(tid) self.config.set("opened_tasks", open_tasks) except KeyError: log.debug('Tried to close tid %s but it is not open', tid) # -------------------------------------------------------------------------- # SHUTDOWN # -------------------------------------------------------------------------- def save_tasks(self): """Save opened tasks and their positions.""" open_task = [] for otid in list(self.open_tasks.keys()): open_task.append(otid) self.open_tasks[otid].close() self.config.set("opened_tasks", open_task) def save_plugin_settings(self): """Save plugin settings to configuration.""" if self.plugin_engine is None: return # Can't save when none has been loaded if self.plugin_engine.plugins: self.config_plugins.set( 'disabled', [p.module_name for p in self.plugin_engine.get_plugins('disabled')]) self.config_plugins.set( 'enabled', [p.module_name for p in self.plugin_engine.get_plugins('enabled')]) self.plugin_engine.deactivate_plugins() def quit(self): """Quit the application.""" # This is needed to avoid warnings when closing the browser # with editor windows open, because of the "win" # group of actions. self.save_tasks() Gtk.Application.quit(self) def do_shutdown(self): """Callback when GTG is closed.""" self.save_plugin_settings() self.ds.save() if self.req is not None: # Save data and shutdown datastore backends self.req.save_datastore(quit=True) Gtk.Application.do_shutdown(self) # -------------------------------------------------------------------------- # MISC # -------------------------------------------------------------------------- @staticmethod def set_logging(debug: bool = False): """Set whenever it should activate debug stuff like logging or not""" level = logging.DEBUG if debug else logging.INFO handler = logging.StreamHandler() formatter = logging.Formatter("%(asctime)s - %(levelname)s - " "%(module)s:%(funcName)s:%(lineno)d - " "%(message)s") handler.setFormatter(formatter) logger_ = logging.getLogger('GTG') handler.setLevel(level) logger_.addHandler(handler) logger_.setLevel(level) if debug: logger_.debug("Debug output enabled.")
class PreferencesDialog: __AUTOSTART_DIRECTORY = os.path.join(xdg_config_home, "autostart") __AUTOSTART_FILE = "gtg.desktop" def __init__(self, config_obj): """Constructor.""" self.config_obj = config_obj self.config = self.config_obj.conf_dict self.builder = gtk.Builder() self.builder.add_from_file(ViewConfig.PREFERENCES_GLADE_FILE) # store references to some objects widgets = { 'dialog': 'PreferencesDialog', 'backend_tree': 'BackendTree', 'plugin_tree': 'PluginTree', 'plugin_about_dialog': 'PluginAboutDialog', 'plugin_configure': 'plugin_configure', 'plugin_depends': 'PluginDepends', 'plugin_config_dialog': 'PluginConfigDialog', 'pref_autostart': 'pref_autostart', 'pref_show_preview': 'pref_show_preview' } for attr, widget in widgets.iteritems(): setattr(self, attr, self.builder.get_object(widget)) # keep a reference to the parent task browser #FIXME: this is not needed and should be removed # self.tb = taskbrowser self.pengine = PluginEngine() # initialize tree models self._init_backend_tree() # this can't happen yet, due to the order of things in # TaskBrowser.__init__(). Happens in activate() instead. # self._init_plugin_tree() pref_signals_dic = self.get_signals_dict() self.builder.connect_signals(pref_signals_dic) def _init_backend_tree(self): """Initialize the BackendTree gtk.TreeView.""" self._refresh_backend_store() # TODO def _refresh_backend_store(self): """Populate a gtk.ListStore with backend information.""" # create and clear a gtk.ListStore for backend information if not hasattr(self, 'backend_store'): # TODO: create the liststore. It should have one column for each # backend. self.backend_store = gtk.ListStore(str) self.backend_store.clear() # TODO def _refresh_plugin_store(self): """Populate a gtk.ListStore with plugin information.""" # create and clear a gtk.ListStore if not hasattr(self, 'plugin_store'): # see constants PLUGINS_COL_* for column meanings self.plugin_store = gtk.ListStore(str, 'gboolean', str, str, str, 'gboolean',) self.plugin_store.clear() # refresh the status of all plugins self.pengine.recheck_plugin_errors(True) # repopulate the store for name, p in self.pengine.plugins.iteritems(): self.plugin_store.append([name, p.enabled, p.full_name, p.short_description, p.description, not p.error,]) # activateable if there is no error def _refresh_preferences_store(self): """Sets the correct value in the preferences checkboxes""" autostart_path = os.path.join(self.__AUTOSTART_DIRECTORY, \ self.__AUTOSTART_FILE) self.pref_autostart.set_active(os.path.isfile(autostart_path)) #This set_active method doesn't even understand what a boolean is! #what a PITA ! if self.config_priv.get("contents_preview_enable"): toset = 1 else: toset = 0 self.pref_show_preview.set_active(toset) def _init_plugin_tree(self): """Initialize the PluginTree gtk.TreeView. The format is modelled after the one used in gedit; see http://git.gnome.org/browse/gedit/tree/gedit/gedit-plugin-mapnager.c """ # force creation of the gtk.ListStore so we can reference it self._refresh_plugin_store() # renderer for the toggle column renderer = gtk.CellRendererToggle() renderer.set_property('xpad', 6) renderer.connect('toggled', self.on_plugin_toggle) # toggle column column = gtk.TreeViewColumn(None, renderer, active=PLUGINS_COL_ENABLED, activatable=PLUGINS_COL_ACTIVATABLE, sensitive=PLUGINS_COL_ACTIVATABLE) self.plugin_tree.append_column(column) # plugin name column column = gtk.TreeViewColumn() column.set_spacing(6) # icon renderer for the plugin name column icon_renderer = gtk.CellRendererPixbuf() icon_renderer.set_property('stock-size', gtk.ICON_SIZE_SMALL_TOOLBAR) icon_renderer.set_property('xpad', 3) column.pack_start(icon_renderer, expand=False) column.set_cell_data_func(icon_renderer, plugin_icon) # text renderer for the plugin name column name_renderer = gtk.CellRendererText() name_renderer.set_property('ellipsize', pango.ELLIPSIZE_END) column.pack_start(name_renderer) column.set_cell_data_func(name_renderer, plugin_markup) self.plugin_tree.append_column(column) # finish setup self.plugin_tree.set_model(self.plugin_store) self.plugin_tree.set_search_column(2) ## GTK signals & related functions def get_signals_dict(self): """A dictionary of signals and functions to be connected.""" SIGNAL_CONNECTIONS_DIC = { # 'on_preferences_activate': # self.activate, # buttons in the dialog itself 'on_prefs_close': self.on_close, 'on_prefs_help': self.on_help, # preferences on the Tasks tab 'on_pref_show_preview_toggled': self.toggle_preview, 'on_pref_check_spelling_toggled': self.toggle_spellcheck, # buttons on the Plugins tab 'on_PluginTree_cursor_changed': self.on_plugin_select, 'on_plugin_about': self.on_plugin_about, 'on_plugin_configure': self.on_plugin_configure, # the PluginAboutDialog 'on_PluginAboutDialog_close': self.on_plugin_about_close, 'on_PluginAboutDialog_response': self.on_plugin_about_close, # the PluginConfigDialog 'on_PluginConfigClose_released': self.on_plugin_config_close, 'on_PreferencesDialog_delete_event': self.on_close, 'on_pref_autostart_toggled': self.on_autostart_toggled, } return SIGNAL_CONNECTIONS_DIC def activate(self, config_priv, widget=None): """Activate the preferences dialog.""" self.config_priv = config_priv if len(self.plugin_tree.get_columns()) == 0: # late setup of PluginTree self._init_plugin_tree() else: self._refresh_plugin_store() self._refresh_backend_store() self._refresh_preferences_store() self.dialog.present() self.dialog.show_all() def on_close(self, widget, data = None): """Close the preferences dialog.""" if self.pengine.get_plugins(): self.config["plugins"] = {} self.config["plugins"]["disabled"] = \ [p.module_name for p in self.pengine.get_plugins("disabled")] self.config["plugins"]["enabled"] = \ [p.module_name for p in self.pengine.get_plugins("enabled")] self.config_obj.save() self.dialog.hide() return True def on_help(self, widget): """Provide help for the preferences dialog.""" return True def on_plugin_about(self, widget): """Display information about a plugin.""" (junk, iter) = self.plugin_tree.get_selection().get_selected() if iter == None: return plugin_id = self.plugin_store.get_value(iter, PLUGINS_COL_ID) p = self.pengine.get_plugin(plugin_id) pad = self.plugin_about_dialog pad.set_name(p.full_name) pad.set_version(p.version) authors = p.authors if isinstance(authors, str): authors = [authors, ] pad.set_authors(authors) pad.set_comments(p.description.replace(r'\n', "\n")) self.plugin_depends.set_label(plugin_error_text(p)) pad.show_all() def on_plugin_about_close(self, widget, *args): """Close the PluginAboutDialog.""" self.plugin_about_dialog.hide() def on_plugin_configure(self, widget): """Configure a plugin.""" (junk, iter) = self.plugin_tree.get_selection().get_selected() plugin_id = self.plugin_store.get_value(iter, PLUGINS_COL_ID) # TODO: load plugin's configuration UI and insert into pc-vbox1 in # position 0. Something like... #pcd = self.plugin_config_dialog #pcd.show_all() # ...for now, use existing code. self.pengine.get_plugin(plugin_id).instance.configure_dialog(self.dialog) def on_plugin_config_close(self, widget): """Close the PluginConfigDialog.""" self.plugin_config_dialog.hide() def on_plugin_select(self, plugin_tree): (model, iter) = plugin_tree.get_selection().get_selected() if iter is not None: plugin_id = model.get_value(iter, PLUGINS_COL_ID) self._update_plugin_configure(self.pengine.get_plugin(plugin_id)) def on_plugin_toggle(self, widget, path): """Toggle a plugin enabled/disabled.""" iter = self.plugin_store.get_iter(path) plugin_id = self.plugin_store.get_value(iter, PLUGINS_COL_ID) p = self.pengine.get_plugin(plugin_id) p.enabled = not self.plugin_store.get_value(iter, PLUGINS_COL_ENABLED) if p.enabled: self.pengine.activate_plugins([p]) else: self.pengine.deactivate_plugins([p]) self.plugin_store.set_value(iter, PLUGINS_COL_ENABLED, p.enabled) self._update_plugin_configure(p) def toggle_preview(self, widget): """Toggle previews in the task view on or off.""" self.config_priv.set("contents_preview_enable",widget.get_active()) def toggle_spellcheck(self, widget): """Toggle spell checking on or off.""" print __name__ def _update_plugin_configure(self, plugin): """Enable the "Configure Plugin" button if appropriate.""" configurable = plugin.active and plugin.is_configurable() self.plugin_configure.set_property('sensitive', configurable) def on_autostart_toggled(self, widget): """Toggle GTG autostarting with the GNOME desktop""" autostart_path = os.path.join(self.__AUTOSTART_DIRECTORY, \ self.__AUTOSTART_FILE) if widget.get_active() == False: #Disable autostart, removing the file in autostart_path if os.path.isfile(autostart_path): os.remove(autostart_path) else: #Enable autostart #We look for the desktop file desktop_file_path = None desktop_file_directories = ["../..", "../../../applications", "../../../../../share/applications"] this_directory = os.path.dirname(os.path.abspath(__file__)) for path in desktop_file_directories: fullpath = os.path.normpath(os.path.join(this_directory, path, \ self.__AUTOSTART_FILE)) if os.path.isfile(fullpath): desktop_file_path = fullpath break #If we have found the desktop file, we make a link to in in # autostart_path. If symbolic linking is not possible # (that is, if we are running on Windows), then copy the file if desktop_file_path: if not os.path.exists(self.__AUTOSTART_DIRECTORY): os.mkdir(self.__AUTOSTART_DIRECTORY) if os.path.isdir(self.__AUTOSTART_DIRECTORY) and \ not os.path.exists(autostart_path): if hasattr(os, "symlink"): os.symlink(desktop_file_path, \ autostart_path) else: shutil.copyfile(desktop_file_path, \ autostart_path)