summaryrefslogtreecommitdiffstats
path: root/deluge/ui/gtk3/torrentdetails.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/ui/gtk3/torrentdetails.py')
-rw-r--r--deluge/ui/gtk3/torrentdetails.py487
1 files changed, 487 insertions, 0 deletions
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 <andrewresch@gmail.com>
+#
+# 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')