From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/ui/gtk3/torrentdetails.py | 487 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 deluge/ui/gtk3/torrentdetails.py (limited to 'deluge/ui/gtk3/torrentdetails.py') diff --git a/deluge/ui/gtk3/torrentdetails.py b/deluge/ui/gtk3/torrentdetails.py new file mode 100644 index 0000000..08c37a1 --- /dev/null +++ b/deluge/ui/gtk3/torrentdetails.py @@ -0,0 +1,487 @@ +# +# Copyright (C) 2007 Andrew Resch +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + + +"""The torrent details component shows info about the selected torrent.""" +import logging +from collections import namedtuple + +from gi.repository.Gtk import ( + CheckMenuItem, + Menu, + MenuItem, + PositionType, + RadioMenuItem, + SeparatorMenuItem, +) + +import deluge.component as component +from deluge.ui.client import client + +from .common import load_pickled_state_file, save_pickled_state_file + +log = logging.getLogger(__name__) + +TabWidget = namedtuple('TabWidget', ('obj', 'func', 'status_keys')) + + +class Tab: + def __init__(self, name=None, child_widget=None, tab_label=None): + self._name = name + self.is_visible = True + self.position = -1 + self.weight = -1 + + self.main_builder = component.get('MainWindow').get_builder() + self._child_widget = ( + self.main_builder.get_object(child_widget) if child_widget else None + ) + self._tab_label = self.main_builder.get_object(tab_label) if tab_label else None + + self.tab_widgets = {} + self.status_keys = [] + + def get_name(self): + return self._name + + def get_child_widget(self): + parent = self._child_widget.get_parent() + if parent is not None: + parent.remove(self._child_widget) + + return self._child_widget + + def get_tab_label(self): + parent = self._tab_label.get_parent() + log.debug('parent: %s', parent) + if parent is not None: + parent.remove(self._tab_label) + + return self._tab_label + + def widget_status_as_fstr(self, widget, status): + """Use TabWidget status_key and func to format status string. + + Args: + widget (TabWidget): A tuple of widget object, func and status_keys. + status (dict): Torrent status dict. + + Returns: + str: The formatted status string. + """ + try: + if widget.func is None: + txt = status[widget.status_keys[0]] + else: + args = [status[key] for key in widget.status_keys] + txt = widget.func(*args) + except KeyError as ex: + log.warning('Unable to get status value: %s', ex) + txt = '' + return txt + + def add_tab_widget(self, widget_id, format_func, status_keys): + """Create TabWidget item in tab_widgets dictionary. + + Args: + widget_id (str): The widget id used to retrieve widget from mainwindow builder. + format_func (str): A func name related to widget e.g. string label formatter. + status_keys (list): List of status keys to lookup for the widget. + + """ + widget_obj = self.main_builder.get_object(widget_id) + self.status_keys.extend(status_keys) + # Store the widget in a tab_widgets dict with name as key for faster lookup. + self.tab_widgets[widget_id] = TabWidget(widget_obj, format_func, status_keys) + + +class TorrentDetails(component.Component): + def __init__(self): + component.Component.__init__(self, 'TorrentDetails', interval=2) + main_builder = component.get('MainWindow').get_builder() + self.config = component.get('MainWindow').config + + self.notebook = main_builder.get_object('torrent_info') + self.notebook.set_tab_pos( + getattr(PositionType, self.config['tabsbar_tab_pos'].upper()) + ) + + # This is the menu item we'll attach the tabs checklist menu to + self.menu_tabs = main_builder.get_object('menu_tabs') + + self.notebook.connect('switch-page', self._on_switch_page) + + # Tabs holds references to the Tab objects by their name + self.tabs = {} + + # Add the default tabs + from .details_tab import DetailsTab + from .files_tab import FilesTab + from .options_tab import OptionsTab + from .peers_tab import PeersTab + from .status_tab import StatusTab + from .trackers_tab import TrackersTab + + default_tabs = { + 'Status': StatusTab, + 'Details': DetailsTab, + 'Files': FilesTab, + 'Peers': PeersTab, + 'Options': OptionsTab, + 'Trackers': TrackersTab, + } + + # tab_name, visible + default_order = [ + ('Status', True), + ('Details', True), + ('Options', True), + ('Files', True), + ('Peers', True), + ('Trackers', True), + ] + + self.translate_tabs = { + 'All': _('_All'), + 'Status': _('_Status'), + 'Details': _('_Details'), + 'Files': _('Fi_les'), + 'Peers': _('_Peers'), + 'Options': _('_Options'), + 'Trackers': _('_Trackers'), + } + + # Get the state from saved file + state = self.load_state() + + if state: + for item in state: + if not isinstance(item, tuple): + log.debug('Old tabs.state, using default..') + state = None + break + + # The state is a list of tab_names in the order they should appear + if state is None: + # Set the default order + state = default_order + + # We need to rename the tab in the state for backwards compat + self.state = [ + (tab_name.replace('Statistics', 'Status'), visible) + for tab_name, visible in state + ] + + for tab in default_tabs.values(): + self.add_tab(tab(), generate_menu=False) + + # Generate the checklist menu + self.generate_menu() + + self.visible(self.config['show_tabsbar']) + + def tab_insert_position(self, weight): + """Returns the position a tab with a given weight should be inserted in""" + # Determine insert position based on weight + # weights is a list of visible tab names in weight order + + weights = sorted( + (tab.weight, name) for name, tab in self.tabs.items() if tab.is_visible + ) + + log.debug('weights: %s', weights) + log.debug('weight of tab: %s', weight) + + position = -1 + for w, name in weights: + if w >= weight: + position = self.tabs[name].position + log.debug('Found pos %d', position) + break + return position + + def add_tab(self, tab, generate_menu=True, visible=None): + name = tab.get_name() + + # find position of tab in self.state, this is the tab weight + weight = None + for w, item in enumerate(self.state): + if item[0] == name: + weight = w + if visible is None: + visible = item[1] + break + + if weight is None: + if visible is None: + visible = True + weight = len(self.state) + self.state.append((name, visible)) + + tab.weight = weight + + if visible: + tab.is_visible = True + # add the tab at position guided by the weight + insert_pos = self.tab_insert_position(weight) + log.debug('Trying to insert tab at %d', insert_pos) + pos = self.notebook.insert_page( + tab.get_child_widget(), tab.get_tab_label(), insert_pos + ) + log.debug('Tab inserted at %d', pos) + tab.position = pos + if not self.notebook.get_property('visible'): + # If the notebook isn't visible, show it + self.visible(True) + else: + tab.is_visible = False + + self.tabs[name] = tab + if name not in self.translate_tabs: + self.translate_tabs[name] = _(name) + + self.regenerate_positions() + if generate_menu: + self.generate_menu() + + def regenerate_positions(self): + """Sync the positions in the tab, with the position stored in the tab object""" + for tab in self.tabs: + page_num = self.notebook.page_num(self.tabs[tab]._child_widget) + if page_num > -1: + self.tabs[tab].position = page_num + + def remove_tab(self, tab_name): + """Removes a tab by name.""" + self.notebook.remove_page(self.tabs[tab_name].position) + del self.tabs[tab_name] + self.regenerate_positions() + self.generate_menu() + + # If there are no tabs visible, then do not show the notebook + if len(self.tabs) == 0: + self.visible(False) + + def hide_all_tabs(self): + """Hides all tabs""" + log.debug('n_pages: %s', self.notebook.get_n_pages()) + for n in range(self.notebook.get_n_pages() - 1, -1, -1): + self.notebook.remove_page(n) + + for tab in self.tabs: + self.tabs[tab].is_visible = False + log.debug('n_pages: %s', self.notebook.get_n_pages()) + self.generate_menu() + self.visible(False) + + def show_all_tabs(self): + """Shows all tabs""" + for tab in self.tabs: + if not self.tabs[tab].is_visible: + self.show_tab(tab, generate_menu=False) + self.generate_menu() + + def hide_tab(self, tab_name): + """Hides tab by name""" + self.tabs[tab_name].is_visible = False + self.notebook.remove_page(self.tabs[tab_name].position) + self.regenerate_positions() + self.generate_menu() + + show = False + for name, tab in self.tabs.items(): + show = show or tab.is_visible + + self.visible(show) + + def show_tab(self, tab_name, generate_menu=True): + log.debug( + '%s\n%s\n%s', + self.tabs[tab_name].get_child_widget(), + self.tabs[tab_name].get_tab_label(), + self.tabs[tab_name].position, + ) + + position = self.tab_insert_position(self.tabs[tab_name].weight) + + log.debug('position: %s', position) + self.notebook.insert_page( + self.tabs[tab_name].get_child_widget(), + self.tabs[tab_name].get_tab_label(), + position, + ) + self.tabs[tab_name].is_visible = True + self.regenerate_positions() + if generate_menu: + self.generate_menu() + self.visible(True) + + def create_tab_pos_menuitem(self): + """Returns a menu to select which side of the notebook the tabs should be shown""" + tab_pos_menu = Menu() + tab_pos_menuitem = MenuItem.new_with_label(_('Position')) + group = [] + for pos in ('top', 'right', 'bottom', 'left'): + menuitem = RadioMenuItem.new_with_mnemonic(group, _(pos.capitalize())) + group = menuitem.get_group() + menuitem.connect('toggled', self._on_tabs_pos_toggled, pos) + menuitem.set_active(pos == self.notebook.get_tab_pos().value_nick) + tab_pos_menu.append(menuitem) + tab_pos_menuitem.set_submenu(tab_pos_menu) + return tab_pos_menuitem + + def generate_menu(self): + """Generates the checklist menu for all the tabs and attaches it""" + menu = Menu() + + # Create 'All' menuitem and a separator + menuitem = CheckMenuItem.new_with_mnemonic(self.translate_tabs['All']) + menuitem.set_name('All') + + all_tabs = True + for key in self.tabs: + if not self.tabs[key].is_visible: + all_tabs = False + break + menuitem.set_active(all_tabs) + menuitem.connect('toggled', self._on_menuitem_toggled) + + menu.append(menuitem) + + menuitem = SeparatorMenuItem() + menu.append(menuitem) + + # Create a list in order of tabs to create menu + menuitem_list = [] + for tab_name in self.tabs: + menuitem_list.append((self.tabs[tab_name].weight, tab_name)) + menuitem_list.sort() + + for pos, name in menuitem_list: + menuitem = CheckMenuItem.new_with_mnemonic(self.translate_tabs[name]) + menuitem.set_name(name) + menuitem.set_active(self.tabs[name].is_visible) + menuitem.connect('toggled', self._on_menuitem_toggled) + menu.append(menuitem) + + menu.append(SeparatorMenuItem()) + menu.append(self.create_tab_pos_menuitem()) + + self.menu_tabs.set_submenu(menu) + self.menu_tabs.show_all() + + def visible(self, visible): + self.notebook.show() if visible else self.notebook.hide() + self.config['show_tabsbar'] = visible + + def set_tab_visible(self, tab_name, visible): + """Sets the tab to visible""" + log.debug('set_tab_visible name: %s visible: %s', tab_name, visible) + if visible and not self.tabs[tab_name].is_visible: + self.show_tab(tab_name) + elif not visible and self.tabs[tab_name].is_visible: + self.hide_tab(tab_name) + + def start(self): + for tab in self.tabs.values(): + try: + tab.start() + except AttributeError: + pass + + def stop(self): + self.clear() + for tab in self.tabs.values(): + try: + tab.stop() + except AttributeError: + pass + + def shutdown(self): + # Save the state of the tabs + for tab in self.tabs: + try: + self.tabs[tab].save_state() + except AttributeError: + pass + + # Save tabs state + self.save_state() + + def update(self, page_num=None): + if len(component.get('TorrentView').get_selected_torrents()) == 0: + # No torrents selected, so just clear + self.clear() + + if self.notebook.get_property('visible'): + if page_num is None: + page_num = self.notebook.get_current_page() + try: + # Get the tab name + name = None + for tab in self.tabs: + if ( + self.tabs[tab].position == page_num + and self.tabs[tab].is_visible + ): + name = tab + except IndexError: + return + # Update the tab that is in view + if name: + self.tabs[name].update() + + def clear(self): + # Get the tab name + try: + page_num = self.notebook.get_current_page() + name = None + for tab in self.tabs: + if self.tabs[tab].position == page_num and self.tabs[tab].is_visible: + name = tab + if name: + self.tabs[name].clear() + except Exception as ex: + log.debug('Unable to clear torrentdetails: %s', ex) + + def _on_switch_page(self, notebook, page, page_num): + self.update(page_num) + client.force_call(False) + + def _on_menuitem_toggled(self, widget): + # Get the tab name + name = widget.get_name() + if name == 'All': + if widget.get_active(): + self.show_all_tabs() + else: + self.hide_all_tabs() + return + + self.set_tab_visible(name, widget.get_active()) + + def _on_tabs_pos_toggled(self, widget, position): + self.config['tabsbar_tab_pos'] = position + self.notebook.set_tab_pos(getattr(PositionType, position.upper())) + + def save_state(self): + """We save the state, which is basically the tab_index list""" + # Update the visiblity status of all tabs + # Leave tabs we dont know anything about it the state as they + # might come from a plugin + for i, (name, visible) in enumerate(self.state): + log.debug('Testing name: %s', name) + if name in self.tabs: + self.state[i] = (name, self.tabs[name].is_visible) + log.debug('Set to %s', self.state[i]) + state = self.state + + save_pickled_state_file('tabs.state', state) + + def load_state(self): + return load_pickled_state_file('tabs.state') -- cgit v1.2.3