def task_new_dialog(self): """Get the new task dialog of the window.""" if self._task_new_dialog is None: self._task_new_dialog = TaskNewDialog( self._pool_model, parent=self, flags=(Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL)) self._task_new_dialog.set_transient_for(self) return self._task_new_dialog
class Toplevel(Gtk.Window, LoggingMixin): """Toplevel window of L{yaner}.""" _UI_FILE = load_ui_file('ui.xml') """The menu and toolbar interfaces, used by L{ui_manager}.""" def __init__(self): """ Create toplevel window of L{yaner}. The window structure is like this: - vbox - toolbar - hpaned - scrolled_window - _pool_view - task_vbox - _task_list_view """ Gtk.Window.__init__(self, title=_('Yaner')) LoggingMixin.__init__(self) self.logger.info('Initializing toplevel window...') self._settings = None self._popups = None # UIManager: Toolbar and menus self._action_group = None self._ui_manager = None self.set_default_size(self.settings.get_uint('width'), self.settings.get_uint('height')) if self.settings.get_boolean('maximized'): self.maximize() self.set_default_icon_name('yaner') # The toplevel vbox vbox = Box(VERTICAL, 0) self.add(vbox) # Toolbar toolbar = self.ui_manager.get_widget('/toolbar') vbox.pack_start(toolbar, expand=False) # HPaned: PoolView as left, TaskVBox as right hpaned = Gtk.HPaned() vbox.pack_start(hpaned) # Right pane vbox = Box(VERTICAL) hpaned.pack2(vbox, True, False) self.task_box = vbox self._task_list_model = TaskListModel() scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_shadow_type(Gtk.ShadowType.IN) scrolled_window.set_size_request(400, -1) vbox.pack_end(scrolled_window) task_list_view = TaskListView(self._task_list_model) task_list_view.set_show_expanders(False) task_list_view.set_level_indentation(16) task_list_view.expand_all() task_list_view.selection.set_mode(Gtk.SelectionMode.MULTIPLE) task_list_view.connect('key-press-event', self._on_task_list_view_key_pressed) task_list_view.connect('button-press-event', self._on_task_list_view_button_pressed) task_list_view.connect('row-activated', self._on_task_list_view_row_activated) scrolled_window.add(task_list_view) self._task_list_view = task_list_view # Left pane scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER) scrolled_window.set_shadow_type(Gtk.ShadowType.IN) scrolled_window.set_size_request(180, -1) hpaned.pack1(scrolled_window, False, False) self._pool_model = PoolModel() pool_view = PoolView(self._pool_model) pool_view.set_headers_visible(False) pool_view.set_show_expanders(False) pool_view.set_level_indentation(16) pool_view.connect('button-press-event', self._on_pool_view_button_pressed) scrolled_window.add(pool_view) self._pool_view = pool_view pool_view.selection.set_mode(Gtk.SelectionMode.SINGLE) pool_view.selection.connect("changed", self._on_pool_view_selection_changed) # Add Pools to the PoolModel for pool in SQLSession.query(Pool): self._pool_model.add_pool(pool) pool_view.expand_all() # Select first iter pool_view.selection.select_iter(self._pool_model.get_iter_first()) # Dialogs self._task_new_dialog = None self._preferences_dialog = None self._about_dialog = None self._category_bar = None self._pool_bar = None # Status icon status_icon = Gtk.StatusIcon(icon_name='yaner') status_icon.connect('activate', self._on_status_icon_activated) status_icon.connect('popup-menu', self._on_status_icon_popup) self.connect('delete-event', self._on_delete_event, status_icon) self.logger.info('Toplevel window initialized.') @property def settings(self): """Get the GSettings object.""" if self._settings is None: self._settings = Gio.Settings('com.kissuki.yaner.ui') return self._settings @property def ui_manager(self): """Get the UI Manager of L{yaner}.""" if self._ui_manager is None: self.logger.info('Initializing UI Manager...') ui_manager = Gtk.UIManager() ui_manager.insert_action_group(self.action_group) try: ui_manager.add_ui_from_file(self._UI_FILE) except GObject.GError: self.logger.exception("Failed to add ui file to UIManager.") SQLSession.close() logging.shutdown() sys.exit(1) else: self.logger.info('UI Manager initialized.') self._ui_manager = ui_manager return self._ui_manager @property def action_group(self): """Get the action group of L{yaner}.""" if self._action_group is None: self.logger.info('Initializing action group...') # The actions used by L{action_group}. The members are: # name, stock-id, label, accelerator, tooltip, callback action_entries = ( ("task_new", 'gtk-new', _("New Task"), None, None, self._on_task_new), ("task_start", 'gtk-media-play', _("Start"), None, None, self._on_task_start), ("task_pause", 'gtk-media-pause', _("Pause"), None, None, self._on_task_pause), ("task_start_all", 'gtk-media-play', _("Start All"), None, None, self._on_task_start_all), ("task_pause_all", 'gtk-media-pause', _("Pause All"), None, None, self._on_task_pause_all), ("task_remove", 'gtk-delete', None, None, None, self._on_task_remove), ("task_restore", 'gtk-undelete', None, None, None, self._on_task_restore), ('category_add', 'gtk-add', _('Add Category'), None, None, self._on_category_add), ('category_edit', 'gtk-edit', _('Edit Category'), None, None, self._on_category_edit), ('category_remove', 'gtk-delete', _('Remove Category'), None, None, self._on_category_remove), ('pool_add', 'gtk-add', _('Add Server'), None, None, self._on_pool_add), ('pool_edit', 'gtk-edit', _('Edit Server'), None, None, self._on_pool_edit), ('pool_remove', 'gtk-delete', _('Remove Server'), None, None, self._on_pool_remove), ('dustbin_empty', 'gtk-delete', _('Empty Dustbin'), None, None, self._on_dustbin_empty), ("toggle_hidden", None, _("Show / Hide"), None, None, self._on_toggle_hidden), ("preferences", "gtk-preferences", None, None, None, self._on_preferences), ("about", "gtk-about", None, None, None, self.about), ("quit", "gtk-quit", None, None, None, self.destroy), ) action_group = Gtk.ActionGroup("ToplevelActions") action_group.add_actions(action_entries, self) self.logger.info('Action group initialized.') self._action_group = action_group return self._action_group @property def popups(self): """Get popup menus, which is a dict.""" if self._popups is None: self.logger.info('Initializing popup menus...') get_widget = self.ui_manager.get_widget popups = {} for popup_name in ('tray', 'pool', 'queuing', 'category', 'dustbin', 'queuing_task', 'category_task', 'dustbin_task'): popups[popup_name] = get_widget('/{}_popup'.format(popup_name)) self._popups = popups self.logger.info('Popup menus initialized.') return self._popups @property def task_new_dialog(self): """Get the new task dialog of the window.""" if self._task_new_dialog is None: self._task_new_dialog = TaskNewDialog( self._pool_model, parent=self, flags=(Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL)) self._task_new_dialog.set_transient_for(self) return self._task_new_dialog @property def preferences_dialog(self): """Get the preferences dialog of the window.""" if self._preferences_dialog is None: self._preferences_dialog = PreferencesDialog( parent=self, flags=(Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL)) self._preferences_dialog.set_transient_for(self) return self._preferences_dialog @property def about_dialog(self): if self._about_dialog is None: about_dialog = Gtk.AboutDialog() about_dialog.set_program_name(_('Yaner')) about_dialog.set_version(__version__) about_dialog.set_logo_icon_name('yaner') about_dialog.set_authors((__author__, )) about_dialog.set_website('https://github.com/iven/Yaner') about_dialog.set_copyright('Copyright \u00a9 2010-2011 Iven Hsu') about_dialog.set_transient_for(self) self._about_dialog = about_dialog return self._about_dialog @property def category_bar(self): """The info bar for adding or editing categories.""" if self._category_bar is None: category_bar = CategoryBar( self._pool_view.selected_presentable.pool, self) category_bar.connect('response', self._on_category_bar_response) self.task_box.pack_end(category_bar, expand=False) self._category_bar = category_bar return self._category_bar @property def pool_bar(self): """The info bar for adding or editing pools.""" if self._pool_bar is None: pool_bar = PoolBar() pool_bar.connect('response', self._on_pool_bar_response) self.task_box.pack_end(pool_bar, expand=False) self._pool_bar = pool_bar return self._pool_bar def _on_status_icon_activated(self, status_icon): """When status icon clicked, switch the window visible or hidden.""" self.logger.debug('Status icon activated.') self.action_group.get_action('toggle_hidden').activate() def _on_status_icon_popup(self, status_icon, button, activate_time): """When status icon right-clicked, show the menu.""" self.logger.debug('Status icon menu popuped.') self.popups['tray'].popup(None, None, None, None, button, activate_time) def _on_toggle_hidden(self, action, data): """Toggle the toplevel window shown or hidden.""" if self.get_property('visible'): self.hide() self.logger.debug('Toplevel window hidden.') else: self.present() self.logger.debug('Toplevel window shown.') def _on_delete_event(self, window, event, status_icon): """When window close button is clicked, try to hide the window instead of quit the application. """ if status_icon.is_embedded(): self.hide() self.logger.debug('Toplevel window hidden.') return True else: return False def _on_task_list_view_row_activated(self, treeview, path, column): """When task row double clicked, start or pause the task.""" model = treeview.get_model() activating_task = model.get_task(model.get_iter(path)) presentable = self._pool_view.selected_presentable if presentable.TYPE == Presentable.TYPES.QUEUING: if activating_task.is_pausable: activating_task.pause() elif activating_task.is_unpausable or activating_task.is_addable: activating_task.start() elif presentable.TYPE == Presentable.TYPES.CATEGORY: pool = presentable.pool if pool.is_local and not activating_task.has_bittorrent: path = os.path.join(activating_task.options['dir'], activating_task.options['out']) self.logger.info('Opening file {}...'.format(path)) xdg_open([path]) def _on_task_list_view_key_pressed(self, treeview, event): if event.keyval == Gdk.KEY_Delete: self._on_task_remove() def _on_task_list_view_button_pressed(self, treeview, event): """Popup menu when necessary.""" selection = treeview.get_selection() (model, paths) = selection.get_selected_rows() current_path = treeview.get_path_at_pos(event.x, event.y) if current_path is None: selection.unselect_all() return True if event.button == 3: # If the clicked row is not selected, select it only if current_path is not None and current_path[0] not in paths: selection.unselect_all() selection.select_path(current_path[0]) popup_dict = { Presentable.TYPES.QUEUING: 'queuing_task', Presentable.TYPES.CATEGORY: 'category_task', Presentable.TYPES.DUSTBIN: 'dustbin_task', } popup_menu = self.popups[popup_dict[model.presentable.TYPE]] popup_menu.popup(None, None, None, None, event.button, event.time) return True return False def _on_pool_view_button_pressed(self, treeview, event): """Popup menu when necessary.""" selection = treeview.get_selection() (model, iter_) = selection.get_selected() current_path = treeview.get_path_at_pos(event.x, event.y) if event.button == 3: if current_path is None: self.popups['pool'].popup(None, None, None, None, event.button, event.time) return True # If the clicked row is not selected, select it if current_path[0] != model.get_path(iter_): selection.select_path(current_path[0]) popup_dict = { Presentable.TYPES.QUEUING: 'queuing', Presentable.TYPES.CATEGORY: 'category', Presentable.TYPES.DUSTBIN: 'dustbin', } popup_menu = self.popups[popup_dict[ treeview.selected_presentable.TYPE]] popup_menu.popup(None, None, None, None, event.button, event.time) return True return False def _on_pool_view_selection_changed(self, selection): """ Pool view tree selection changed signal callback. Update the task list model. """ presentable = self._pool_view.selected_presentable if presentable is not None: self._task_list_model.presentable = presentable def _on_preferences(self, action, data): """When preferences action is activated, call the preferences dialog.""" self.preferences_dialog.run() def _on_task_new(self, action, data): """When task new action is activated, call the task new dialog.""" self.task_new_dialog.run() def _on_task_start(self, action, data): """When task start button clicked, start or unpause the task.""" for task in self._task_list_view.selected_tasks: task.start() def _on_task_pause(self, action, data): """When task pause button clicked, pause the task.""" for task in self._task_list_view.selected_tasks: task.pause() def _on_task_start_all(self, action, data): """Start or unpause all the tasks in the selected pool.""" presentable = self._pool_view.selected_presentable if presentable is None or presentable.TYPE != Presentable.TYPES.QUEUING: pools = SQLSession.query(Pool) else: pools = [presentable.pool] for pool in pools: for task in pool.queuing.tasks: task.start() def _on_task_pause_all(self, action, data): """Pause all the tasks in the selected pool.""" presentable = self._pool_view.selected_presentable if presentable is None or presentable.TYPE != Presentable.TYPES.QUEUING: pools = SQLSession.query(Pool) else: pools = [presentable.pool] for pool in pools: for task in pool.queuing.tasks: task.pause() def _on_task_remove(self, action=None, data=None): """When task remove button clicked, remove the task.""" tasks = self._task_list_view.selected_tasks if not tasks: return if self._pool_view.selected_presentable.TYPE == Presentable.TYPES.DUSTBIN: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, _('Are you sure to remove these tasks?'), ) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: for task in tasks: task.remove() else: for task in tasks: task.trash() def _on_task_restore(self, action, data): """When task is removed, restore the task.""" for task in self._task_list_view.selected_tasks: task.restore() def do_configure_event(self, event): """When window size changed, save it in GSettings.""" settings = self.settings if not settings.get_boolean('maximized'): settings.set_uint('width', event.width) settings.set_uint('height', event.height) Gtk.Window.do_configure_event(self, event) def do_window_state_event(self, event): """When window maximized, save it in GSettings.""" maximized = event.new_window_state & Gdk.WindowState.MAXIMIZED self.settings.set_boolean('maximized', maximized) Gtk.Window.do_window_state_event(self, event) def _on_dustbin_empty(self, action, data): """Empty dustbin.""" if self._pool_view.selected_presentable.TYPE == Presentable.TYPES.DUSTBIN: self._task_list_view.get_selection().select_all() self.action_group.get_action('task_remove').activate() def _on_category_add(self, action, data): """Add category.""" presentable = self._pool_view.selected_presentable self.category_bar.update(presentable.pool) self.category_bar.show_all() def _on_category_edit(self, action, data): """Edit category.""" presentable = self._pool_view.selected_presentable self.category_bar.update(presentable.pool, presentable) self.category_bar.show_all() def _on_category_remove(self, action, data): """Remove category.""" category = self._pool_view.selected_presentable pool = category.pool if category is pool.default_category: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, _('The default category should not be removed.'), ) else: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, _('Are you sure to remove the category "{}"?\nAll tasks ' 'in the category will be moved to the default category.'). format(category.name), ) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: # Move all tasks to default category for task in category.tasks: task.category = pool.default_category pool.default_category.add_task(task) # Remove the category iter self._pool_model.remove_presentable(category) SQLSession.delete(category) SQLSession.commit() def _on_category_bar_response(self, info_bar, response_id): """When category_bar responsed, create or edit category.""" if response_id != Gtk.ResponseType.OK: info_bar.hide() return category = info_bar.category pool = info_bar.pool widgets = info_bar.widgets name = widgets['name'].get_text().strip() directory = widgets['directory'].get_text().strip() if not name: widgets['name'].set_placeholder_text(_('Required')) return if not directory: widgets['directory'].set_placeholder_text(_('Required')) return if category is None: category = Category(name=name, directory=directory, pool=pool) self._pool_model.add_presentable(category, insert=True) else: category.name = name category.directory = directory SQLSession.commit() info_bar.hide() def _on_pool_add(self, action, data): """Add pool.""" self.pool_bar.update(None) self.pool_bar.show_all() def _on_pool_edit(self, action, data): """Edit pool.""" presentable = self._pool_view.selected_presentable self.pool_bar.update(presentable.pool) self.pool_bar.show_all() def _on_pool_remove(self, action, data): """Remove pool.""" queuing = self._pool_view.selected_presentable pool = queuing.pool if pool.is_local: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, _('The local server should not be removed.'), ) else: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, _('Are you sure to remove the server "{}"?\nAll tasks ' 'in the server will be <b>removed!</b>').format(pool.name), use_markup=True) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: # Select the local pool, in order to remove the selected pool local_pool = SQLSession.query(Pool).filter( Pool.is_local == True)[0] iter_ = self._pool_model.get_iter_for_presentable( local_pool.queuing) self._pool_view.selection.select_iter(iter_) # Remove the category iter self._pool_model.remove_pool(pool) SQLSession.delete(pool) SQLSession.commit() def _on_pool_bar_response(self, info_bar, response_id): """When pool bar responsed, create or edit pool.""" if response_id != Gtk.ResponseType.OK: info_bar.hide() return pool = info_bar.pool widgets = info_bar.widgets props = {} for (prop, widget) in widgets.items(): props[prop] = widget.get_text().strip() for prop in ('name', 'host', 'port'): if not props[prop]: widgets[prop].set_placeholder_text(_('Required')) return if pool is None: pool = Pool(**props) self._pool_model.add_pool(pool) self._pool_view.expand_all() else: pool.name = props['name'] pool.host = props['host'] pool.port = props['port'] pool.user = props['user'] pool.passwd = props['passwd'] SQLSession.commit() info_bar.hide() def about(self, *args, **kwargs): """Show about dialog.""" self.about_dialog.run() self.about_dialog.hide() def destroy(self, *args, **kwargs): """Destroy toplevel window and quit the application.""" Gtk.Window.destroy(self) self.logger.debug('Window destroyed.')