summaryrefslogtreecommitdiffstats
path: root/deluge/ui/gtk3/torrentview.py
diff options
context:
space:
mode:
authorCalum Lind <calumlind+deluge@gmail.com>2018-07-13 09:34:49 +0100
committerCalum Lind <calumlind@gmail.com>2018-11-02 08:45:39 +0000
commit01fafd4fe0a41c53345d55b34ef16f087382ceb1 (patch)
treed2b3bd253b9e917811f6a1a974078db89d7bb4c8 /deluge/ui/gtk3/torrentview.py
parentca0db4d1a72b38f66de5c85ab3f99438c2f8d0e9 (diff)
downloaddeluge-01fafd4fe0a41c53345d55b34ef16f087382ceb1.tar.gz
deluge-01fafd4fe0a41c53345d55b34ef16f087382ceb1.tar.bz2
deluge-01fafd4fe0a41c53345d55b34ef16f087382ceb1.zip
[GTK3] Change module structure from ui/gtkui to ui/gtk3
This moves the directory structure so that there is no conflict with the old gtk2 UI. Also changes the conf and state files being loaded.
Diffstat (limited to 'deluge/ui/gtk3/torrentview.py')
-rw-r--r--deluge/ui/gtk3/torrentview.py932
1 files changed, 932 insertions, 0 deletions
diff --git a/deluge/ui/gtk3/torrentview.py b/deluge/ui/gtk3/torrentview.py
new file mode 100644
index 000000000..9eef68d9d
--- /dev/null
+++ b/deluge/ui/gtk3/torrentview.py
@@ -0,0 +1,932 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007, 2008 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 view component that lists all torrents in the session."""
+from __future__ import unicode_literals
+
+import logging
+from locale import strcoll
+
+from gi.repository.Gdk import ModifierType, keyval_name
+from gi.repository.GObject import TYPE_UINT64, idle_add
+from gi.repository.Gtk import EntryIconPosition
+from twisted.internet import reactor
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import torrentview_data_funcs as funcs
+from .listview import ListView
+from .removetorrentdialog import RemoveTorrentDialog
+
+log = logging.getLogger(__name__)
+
+try:
+ CTRL_ALT_MASK = ModifierType.CONTROL_MASK | ModifierType.MOD1_MASK
+except TypeError:
+ # Sphinx AutoDoc has a mock issue with gtk.gdk masks.
+ pass
+
+
+def str_nocase_sort(model, iter1, iter2, data):
+ """Sort string column data using ISO 14651 in lowercase.
+
+ Uses locale.strcoll which (allegedly) uses ISO 14651. Compares first
+ value with second and returns -1, 0, 1 for where it should be placed.
+
+ """
+ v1 = model[iter1][data]
+ v2 = model[iter2][data]
+ # Catch any values of None from model.
+ v1 = v1.lower() if v1 else ''
+ v2 = v2.lower() if v2 else ''
+ return strcoll(v1, v2)
+
+
+def queue_peer_seed_sort_function(v1, v2):
+ if v1 == v2:
+ return 0
+ if v2 < 0:
+ return -1
+ if v1 < 0:
+ return 1
+ if v1 > v2:
+ return 1
+ if v2 > v1:
+ return -1
+
+
+def queue_column_sort(model, iter1, iter2, data):
+ v1 = model[iter1][data]
+ v2 = model[iter2][data]
+ return queue_peer_seed_sort_function(v1, v2)
+
+
+def eta_column_sort(model, iter1, iter2, data):
+ v1 = model[iter1][data]
+ v2 = model[iter2][data]
+ if v1 == v2:
+ return 0
+ if v1 == 0:
+ return 1
+ if v2 == 0:
+ return -1
+ if v1 > v2:
+ return 1
+ if v2 > v1:
+ return -1
+
+
+def seed_peer_column_sort(model, iter1, iter2, data):
+ v1 = model[iter1][data] # num seeds/peers
+ v3 = model[iter2][data] # num seeds/peers
+ if v1 == v3:
+ v2 = model[iter1][data + 1] # total seeds/peers
+ v4 = model[iter2][data + 1] # total seeds/peers
+ return queue_peer_seed_sort_function(v2, v4)
+ return queue_peer_seed_sort_function(v1, v3)
+
+
+def progress_sort(model, iter1, iter2, sort_column_id):
+ progress1 = model[iter1][sort_column_id]
+ progress2 = model[iter2][sort_column_id]
+ # Progress value is equal, so sort on state
+ if progress1 == progress2:
+ state1 = model[iter1][sort_column_id + 1]
+ state2 = model[iter2][sort_column_id + 1]
+ return cmp(state1, state2)
+ return cmp(progress1, progress2)
+
+
+class SearchBox(object):
+ def __init__(self, torrentview):
+ self.torrentview = torrentview
+ mainwindow = component.get('MainWindow')
+ main_builder = mainwindow.get_builder()
+
+ self.visible = False
+ self.search_pending = self.prefiltered = None
+
+ self.search_box = main_builder.get_object('search_box')
+ self.search_torrents_entry = main_builder.get_object('search_torrents_entry')
+ self.close_search_button = main_builder.get_object('close_search_button')
+ self.match_search_button = main_builder.get_object('search_torrents_match')
+ mainwindow.connect_signals(self)
+
+ def show(self):
+ self.visible = True
+ self.search_box.show_all()
+ self.search_torrents_entry.grab_focus()
+
+ def hide(self):
+ self.visible = False
+ self.clear_search()
+ self.search_box.hide()
+ self.search_pending = self.prefiltered = None
+
+ def clear_search(self):
+ if self.search_pending and self.search_pending.active():
+ self.search_pending.cancel()
+
+ if self.prefiltered:
+ filter_column = self.torrentview.columns['filter'].column_indices[0]
+ torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0]
+ for row in self.torrentview.liststore:
+ torrent_id = row[torrent_id_column]
+
+ if torrent_id in self.prefiltered:
+ # Reset to previous filter state
+ self.prefiltered.pop(self.prefiltered.index(torrent_id))
+ row[filter_column] = not row[filter_column]
+
+ self.prefiltered = None
+
+ self.search_torrents_entry.set_text('')
+ if self.torrentview.filter and 'name' in self.torrentview.filter:
+ self.torrentview.filter.pop('name', None)
+ self.search_pending = reactor.callLater(0.5, self.torrentview.update)
+
+ def set_search_filter(self):
+ if self.search_pending and self.search_pending.active():
+ self.search_pending.cancel()
+
+ if self.torrentview.filter and 'name' in self.torrentview.filter:
+ self.torrentview.filter.pop('name', None)
+
+ elif self.torrentview.filter is None:
+ self.torrentview.filter = {}
+
+ search_string = self.search_torrents_entry.get_text()
+ if not search_string:
+ self.clear_search()
+ else:
+ if self.match_search_button.get_active():
+ search_string += '::match'
+ self.torrentview.filter['name'] = search_string
+ self.prefilter_torrentview()
+
+ def prefilter_torrentview(self):
+ filter_column = self.torrentview.columns['filter'].column_indices[0]
+ torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0]
+ torrent_name_column = self.torrentview.columns[_('Name')].column_indices[1]
+
+ match_case = self.match_search_button.get_active()
+ if match_case:
+ search_string = self.search_torrents_entry.get_text()
+ else:
+ search_string = self.search_torrents_entry.get_text().lower()
+
+ if self.prefiltered is None:
+ self.prefiltered = []
+
+ for row in self.torrentview.liststore:
+ torrent_id = row[torrent_id_column]
+
+ if torrent_id in self.prefiltered:
+ # Reset to previous filter state
+ self.prefiltered.pop(self.prefiltered.index(torrent_id))
+ row[filter_column] = not row[filter_column]
+
+ if not row[filter_column]:
+ # Row is not visible(filtered out, but not by our filter), skip it
+ continue
+
+ if match_case:
+ torrent_name = row[torrent_name_column]
+ else:
+ torrent_name = row[torrent_name_column].lower()
+
+ if search_string in torrent_name and not row[filter_column]:
+ row[filter_column] = True
+ self.prefiltered.append(torrent_id)
+ elif search_string not in torrent_name and row[filter_column]:
+ row[filter_column] = False
+ self.prefiltered.append(torrent_id)
+
+ def on_close_search_button_clicked(self, widget):
+ self.hide()
+
+ def on_search_filter_toggle(self, widget):
+ if self.visible:
+ self.hide()
+ else:
+ self.show()
+
+ def on_search_torrents_match_toggled(self, widget):
+ if self.search_torrents_entry.get_text():
+ self.set_search_filter()
+ self.search_pending = reactor.callLater(0.7, self.torrentview.update)
+
+ def on_search_torrents_entry_icon_press(self, entry, icon, event):
+ if icon != EntryIconPosition.SECONDARY:
+ return
+ self.clear_search()
+
+ def on_search_torrents_entry_changed(self, widget):
+ self.set_search_filter()
+ self.search_pending = reactor.callLater(0.7, self.torrentview.update)
+
+
+class TorrentView(ListView, component.Component):
+ """TorrentView handles the listing of torrents."""
+
+ def __init__(self):
+ component.Component.__init__(
+ self, 'TorrentView', interval=2, depend=['SessionProxy']
+ )
+ main_builder = component.get('MainWindow').get_builder()
+ # Call the ListView constructor
+ ListView.__init__(
+ self, main_builder.get_object('torrent_view'), 'torrentview.state'
+ )
+ log.debug('TorrentView Init..')
+
+ # If we have gotten the state yet
+ self.got_state = False
+
+ # This is where status updates are put
+ self.status = {}
+
+ # We keep a copy of the previous status to compare for changes
+ self.prev_status = {}
+
+ # Register the columns menu with the listview so it gets updated accordingly.
+ self.register_checklist_menu(main_builder.get_object('menu_columns'))
+
+ # Add the columns to the listview
+ self.add_text_column('torrent_id', hidden=True, unique=True)
+ self.add_bool_column('dirty', hidden=True)
+ self.add_func_column(
+ '#',
+ funcs.cell_data_queue,
+ [int],
+ status_field=['queue'],
+ sort_func=queue_column_sort,
+ )
+ self.add_texticon_column(
+ _('Name'),
+ status_field=['state', 'name'],
+ function=funcs.cell_data_statusicon,
+ sort_func=str_nocase_sort,
+ default_sort=True,
+ )
+ self.add_func_column(
+ _('Size'),
+ funcs.cell_data_size,
+ [TYPE_UINT64],
+ status_field=['total_wanted'],
+ )
+ self.add_func_column(
+ _('Downloaded'),
+ funcs.cell_data_size,
+ [TYPE_UINT64],
+ status_field=['all_time_download'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Uploaded'),
+ funcs.cell_data_size,
+ [TYPE_UINT64],
+ status_field=['total_uploaded'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Remaining'),
+ funcs.cell_data_size,
+ [TYPE_UINT64],
+ status_field=['total_remaining'],
+ default=False,
+ )
+ self.add_progress_column(
+ _('Progress'),
+ status_field=['progress', 'state'],
+ col_types=[float, str],
+ function=funcs.cell_data_progress,
+ sort_func=progress_sort,
+ )
+ self.add_func_column(
+ _('Seeds'),
+ funcs.cell_data_peer,
+ [int, int],
+ status_field=['num_seeds', 'total_seeds'],
+ sort_func=seed_peer_column_sort,
+ default=False,
+ )
+ self.add_func_column(
+ _('Peers'),
+ funcs.cell_data_peer,
+ [int, int],
+ status_field=['num_peers', 'total_peers'],
+ sort_func=seed_peer_column_sort,
+ default=False,
+ )
+ self.add_func_column(
+ _('Seeds:Peers'),
+ funcs.cell_data_ratio_seeds_peers,
+ [float],
+ status_field=['seeds_peers_ratio'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Down Speed'),
+ funcs.cell_data_speed_down,
+ [int],
+ status_field=['download_payload_rate'],
+ )
+ self.add_func_column(
+ _('Up Speed'),
+ funcs.cell_data_speed_up,
+ [int],
+ status_field=['upload_payload_rate'],
+ )
+ self.add_func_column(
+ _('Down Limit'),
+ funcs.cell_data_speed_limit_down,
+ [float],
+ status_field=['max_download_speed'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Up Limit'),
+ funcs.cell_data_speed_limit_up,
+ [float],
+ status_field=['max_upload_speed'],
+ default=False,
+ )
+ self.add_func_column(
+ _('ETA'),
+ funcs.cell_data_time,
+ [int],
+ status_field=['eta'],
+ sort_func=eta_column_sort,
+ )
+ self.add_func_column(
+ _('Ratio'),
+ funcs.cell_data_ratio_ratio,
+ [float],
+ status_field=['ratio'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Avail'),
+ funcs.cell_data_ratio_avail,
+ [float],
+ status_field=['distributed_copies'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Added'),
+ funcs.cell_data_date_added,
+ [int],
+ status_field=['time_added'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Completed'),
+ funcs.cell_data_date_completed,
+ [int],
+ status_field=['completed_time'],
+ default=False,
+ )
+ self.add_func_column(
+ _('Complete Seen'),
+ funcs.cell_data_date_or_never,
+ [int],
+ status_field=['last_seen_complete'],
+ default=False,
+ )
+ self.add_texticon_column(
+ _('Tracker'),
+ function=funcs.cell_data_trackericon,
+ status_field=['tracker_host', 'tracker_host'],
+ default=False,
+ )
+ self.add_text_column(
+ _('Download Folder'), status_field=['download_location'], default=False
+ )
+ self.add_text_column(_('Owner'), status_field=['owner'], default=False)
+ self.add_bool_column(
+ _('Shared'),
+ status_field=['shared'],
+ default=False,
+ tooltip=_('Torrent is shared between other Deluge users or not.'),
+ )
+ self.restore_columns_order_from_state()
+
+ # Set filter to None for now
+ self.filter = None
+
+ # Connect Signals #
+ # Connect to the 'button-press-event' to know when to bring up the
+ # torrent menu popup.
+ self.treeview.connect('button-press-event', self.on_button_press_event)
+ # Connect to the 'key-press-event' to know when the bring up the
+ # torrent menu popup via keypress.
+ self.treeview.connect('key-release-event', self.on_key_press_event)
+ # Connect to the 'changed' event of TreeViewSelection to get selection
+ # changes.
+ self.treeview.get_selection().connect('changed', self.on_selection_changed)
+
+ self.treeview.connect('drag-drop', self.on_drag_drop)
+ self.treeview.connect('drag_data_received', self.on_drag_data_received)
+ self.treeview.connect('key-press-event', self.on_key_press_event)
+ self.treeview.connect('columns-changed', self.on_columns_changed_event)
+
+ self.search_box = SearchBox(self)
+ self.permanent_status_keys = ['owner']
+ self.columns_to_update = []
+
+ def start(self):
+ """Start the torrentview"""
+ # We need to get the core session state to know which torrents are in
+ # the session so we can add them to our list.
+ # Only get the status fields required for the visible columns
+ status_fields = []
+ for listview_column in self.columns.values():
+ if listview_column.column.get_visible():
+ if not listview_column.status_field:
+ continue
+ status_fields.extend(listview_column.status_field)
+ component.get('SessionProxy').get_torrents_status(
+ {}, status_fields
+ ).addCallback(self._on_session_state)
+
+ client.register_event_handler(
+ 'TorrentStateChangedEvent', self.on_torrentstatechanged_event
+ )
+ client.register_event_handler('TorrentAddedEvent', self.on_torrentadded_event)
+ client.register_event_handler(
+ 'TorrentRemovedEvent', self.on_torrentremoved_event
+ )
+ client.register_event_handler('SessionPausedEvent', self.on_sessionpaused_event)
+ client.register_event_handler(
+ 'SessionResumedEvent', self.on_sessionresumed_event
+ )
+ client.register_event_handler(
+ 'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event
+ )
+
+ def _on_session_state(self, state):
+ self.add_rows(state)
+ self.got_state = True
+ # Update the view right away with our status
+ self.status = state
+ self.set_columns_to_update()
+ self.update_view(load_new_list=True)
+ self.select_first_row()
+
+ def stop(self):
+ """Stops the torrentview"""
+ client.deregister_event_handler(
+ 'TorrentStateChangedEvent', self.on_torrentstatechanged_event
+ )
+ client.deregister_event_handler('TorrentAddedEvent', self.on_torrentadded_event)
+ client.deregister_event_handler(
+ 'TorrentRemovedEvent', self.on_torrentremoved_event
+ )
+ client.deregister_event_handler(
+ 'SessionPausedEvent', self.on_sessionpaused_event
+ )
+ client.deregister_event_handler(
+ 'SessionResumedEvent', self.on_sessionresumed_event
+ )
+ client.deregister_event_handler(
+ 'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event
+ )
+
+ if self.treeview.get_selection():
+ self.treeview.get_selection().unselect_all()
+
+ # Save column state before clearing liststore
+ # so column sort details are correctly saved.
+ self.save_state()
+ self.liststore.clear()
+ self.prev_status = {}
+ self.filter = None
+ self.search_box.hide()
+
+ def shutdown(self):
+ """Called when GtkUi is exiting"""
+ pass
+
+ def save_state(self):
+ """
+ Saves the state of the torrent view.
+ """
+ if component.get('MainWindow').visible():
+ ListView.save_state(self, 'torrentview.state')
+
+ def remove_column(self, header):
+ """Removes the column with the name 'header' from the torrentview"""
+ self.save_state()
+ ListView.remove_column(self, header)
+
+ def set_filter(self, filter_dict):
+ """
+ Sets filters for the torrentview..
+
+ see: core.get_torrents_status
+ """
+ search_filter = self.filter and self.filter.get('name', None) or None
+ self.filter = dict(filter_dict) # Copied version of filter_dict.
+ if search_filter and 'name' not in filter_dict:
+ self.filter['name'] = search_filter
+ self.update(select_row=True)
+
+ def set_columns_to_update(self, columns=None):
+ status_keys = []
+ self.columns_to_update = []
+
+ if columns is None:
+ # We need to iterate through all columns
+ columns = list(self.columns)
+
+ # Iterate through supplied list of columns to update
+ for column in columns:
+ # Make sure column is visible and has 'status_field' set.
+ # If not, we can ignore it.
+ if (
+ self.columns[column].column.get_visible() is True
+ and self.columns[column].hidden is False
+ and self.columns[column].status_field is not None
+ ):
+ for field in self.columns[column].status_field:
+ status_keys.append(field)
+ self.columns_to_update.append(column)
+
+ # Remove duplicates
+ self.columns_to_update = list(set(self.columns_to_update))
+ status_keys = list(set(status_keys + self.permanent_status_keys))
+ return status_keys
+
+ def send_status_request(self, columns=None, select_row=False):
+ # Store the 'status_fields' we need to send to core
+ status_keys = self.set_columns_to_update(columns)
+
+ # If there is nothing in status_keys then we must not continue
+ if status_keys is []:
+ return
+
+ # Remove duplicates from status_key list
+ status_keys = list(set(status_keys))
+
+ # Request the statuses for all these torrent_ids, this is async so we
+ # will deal with the return in a signal callback.
+ d = (
+ component.get('SessionProxy')
+ .get_torrents_status(self.filter, status_keys)
+ .addCallback(self._on_get_torrents_status)
+ )
+ if select_row:
+ d.addCallback(self.select_first_row)
+
+ def select_first_row(self, ignored=None):
+ """
+ Set the first row in the list selected if a selection does
+ not already exist
+ """
+ rows = self.treeview.get_selection().get_selected_rows()[1]
+ # Only select row if noe rows are selected
+ if not rows:
+ self.treeview.get_selection().select_path((0,))
+
+ def update(self, select_row=False):
+ """
+ Sends a status request to core and updates the torrent list with the result.
+
+ :param select_row: if the first row in the list should be selected if
+ no rows are already selected.
+ :type select_row: boolean
+
+ """
+ if self.got_state:
+ if (
+ self.search_box.search_pending is not None
+ and self.search_box.search_pending.active()
+ ):
+ # An update request is scheduled, let's wait for that one
+ return
+ # Send a status request
+ idle_add(self.send_status_request, None, select_row)
+
+ def update_view(self, load_new_list=False):
+ """Update the torrent view model with data we've received."""
+ filter_column = self.columns['filter'].column_indices[0]
+ status = self.status
+
+ if not load_new_list:
+ # Freeze notications while updating
+ self.treeview.freeze_child_notify()
+
+ # Get the columns to update from one of the torrents
+ if status:
+ torrent_id = list(status)[0]
+ fields_to_update = []
+ for column in self.columns_to_update:
+ column_index = self.get_column_index(column)
+ for i, status_field in enumerate(self.columns[column].status_field):
+ # Only use columns that the torrent has in the state
+ if status_field in status[torrent_id]:
+ fields_to_update.append((column_index[i], status_field))
+
+ for row in self.liststore:
+ torrent_id = row[self.columns['torrent_id'].column_indices[0]]
+ # We expect the torrent_id to be in status and prev_status,
+ # as it will be as long as the list isn't changed by the user
+
+ torrent_id_in_status = False
+ try:
+ torrent_status = status[torrent_id]
+ torrent_id_in_status = True
+ if torrent_status == self.prev_status[torrent_id]:
+ # The status dict is the same, so do nothing to update for this torrent
+ continue
+ except KeyError:
+ pass
+
+ if not torrent_id_in_status:
+ if row[filter_column] is True:
+ row[filter_column] = False
+ else:
+ if row[filter_column] is False:
+ row[filter_column] = True
+
+ # Find the fields to update
+ to_update = []
+ for i, status_field in fields_to_update:
+ row_value = status[torrent_id][status_field]
+ # FIXME: Seeing UnicodeWarning??
+ if row[i] != row_value:
+ to_update.append(i)
+ to_update.append(row_value)
+ # Update fields in the liststore
+ if to_update:
+ self.liststore.set(row.iter, *to_update)
+
+ if load_new_list:
+ # Create the model filter. This sets the model for the treeview and enables sorting.
+ self.create_model_filter()
+ else:
+ self.treeview.thaw_child_notify()
+
+ component.get('MenuBar').update_menu()
+ self.prev_status = status
+
+ def _on_get_torrents_status(self, status, select_row=False):
+ """Callback function for get_torrents_status(). 'status' should be a
+ dictionary of {torrent_id: {key, value}}."""
+ self.status = status
+ if self.search_box.prefiltered is not None:
+ self.search_box.prefiltered = None
+
+ if self.status == self.prev_status and self.prev_status:
+ # We do not bother updating since the status hasn't changed
+ self.prev_status = self.status
+ return
+ self.update_view()
+
+ def add_rows(self, torrent_ids):
+ """Accepts a list of torrent_ids to add to self.liststore"""
+ torrent_id_column = self.columns['torrent_id'].column_indices[0]
+ dirty_column = self.columns['dirty'].column_indices[0]
+ filter_column = self.columns['filter'].column_indices[0]
+ for torrent_id in torrent_ids:
+ # Insert a new row to the liststore
+ row = self.liststore.append()
+ self.liststore.set(
+ row,
+ torrent_id_column,
+ torrent_id,
+ dirty_column,
+ True,
+ filter_column,
+ True,
+ )
+
+ def remove_row(self, torrent_id):
+ """Removes a row with torrent_id"""
+ for row in self.liststore:
+ if row[self.columns['torrent_id'].column_indices[0]] == torrent_id:
+ self.liststore.remove(row.iter)
+ # Force an update of the torrentview
+ self.update()
+ break
+
+ def mark_dirty(self, torrent_id=None):
+ for row in self.liststore:
+ if (
+ not torrent_id
+ or row[self.columns['torrent_id'].column_indices[0]] == torrent_id
+ ):
+ # log.debug('marking %s dirty', torrent_id)
+ row[self.columns['dirty'].column_indices[0]] = True
+ if torrent_id:
+ break
+
+ def get_selected_torrent(self):
+ """Returns a torrent_id or None. If multiple torrents are selected,
+ it will return the torrent_id of the first one."""
+ selected = self.get_selected_torrents()
+ if selected:
+ return selected[0]
+ else:
+ return selected
+
+ def get_selected_torrents(self):
+ """Returns a list of selected torrents or None"""
+ torrent_ids = []
+ try:
+ paths = self.treeview.get_selection().get_selected_rows()[1]
+ except AttributeError:
+ # paths is likely None .. so lets return []
+ return []
+ try:
+ for path in paths:
+ try:
+ row = self.treeview.get_model().get_iter(path)
+ except Exception as ex:
+ log.debug('Unable to get iter from path: %s', ex)
+ continue
+
+ child_row = self.treeview.get_model().convert_iter_to_child_iter(row)
+ child_row = (
+ self.treeview.get_model()
+ .get_model()
+ .convert_iter_to_child_iter(child_row)
+ )
+ if self.liststore.iter_is_valid(child_row):
+ try:
+ value = self.liststore.get_value(
+ child_row, self.columns['torrent_id'].column_indices[0]
+ )
+ except Exception as ex:
+ log.debug('Unable to get value from row: %s', ex)
+ else:
+ torrent_ids.append(value)
+ if len(torrent_ids) == 0:
+ return []
+
+ return torrent_ids
+ except (ValueError, TypeError):
+ return []
+
+ def get_torrent_status(self, torrent_id):
+ """Returns data stored in self.status, it may not be complete"""
+ try:
+ return self.status[torrent_id]
+ except KeyError:
+ return {}
+
+ def get_visible_torrents(self):
+ return list(self.status)
+
+ # Callbacks #
+ def on_button_press_event(self, widget, event):
+ """This is a callback for showing the right-click context menu."""
+ log.debug('on_button_press_event')
+ # We only care about right-clicks
+ if event.button == 3:
+ x, y = event.get_coords()
+ path = self.treeview.get_path_at_pos(int(x), int(y))
+ if not path:
+ return
+ row = self.model_filter.get_iter(path[0])
+
+ if self.get_selected_torrents():
+ if (
+ self.model_filter.get_value(
+ row, self.columns['torrent_id'].column_indices[0]
+ )
+ not in self.get_selected_torrents()
+ ):
+ self.treeview.get_selection().unselect_all()
+ self.treeview.get_selection().select_iter(row)
+ else:
+ self.treeview.get_selection().select_iter(row)
+ torrentmenu = component.get('MenuBar').torrentmenu
+ torrentmenu.popup(None, None, None, None, event.button, event.time)
+ return True
+
+ def on_selection_changed(self, treeselection):
+ """This callback is know when the selection has changed."""
+ log.debug('on_selection_changed')
+ component.get('TorrentDetails').update()
+ component.get('MenuBar').update_menu()
+
+ def on_drag_drop(self, widget, drag_context, x, y, timestamp):
+ widget.stop_emission('drag-drop')
+
+ def on_drag_data_received(
+ self, widget, drag_context, x, y, selection_data, info, timestamp
+ ):
+ widget.stop_emission('drag_data_received')
+
+ def on_columns_changed_event(self, treeview):
+ log.debug('Treeview Columns Changed')
+ self.save_state()
+
+ def on_torrentadded_event(self, torrent_id, from_state):
+ self.add_rows([torrent_id])
+ self.update()
+
+ def on_torrentremoved_event(self, torrent_id):
+ self.remove_row(torrent_id)
+
+ def on_torrentstatechanged_event(self, torrent_id, state):
+ # Update the torrents state
+ for row in self.liststore:
+ if torrent_id != row[self.columns['torrent_id'].column_indices[0]]:
+ continue
+
+ for name in self.columns_to_update:
+ if not self.columns[name].status_field:
+ continue
+ for idx, status_field in enumerate(self.columns[name].status_field):
+ # Update all columns that use the state field to current state
+ if status_field != 'state':
+ continue
+ row[self.get_column_index(name)[idx]] = state
+
+ if self.filter.get('state', None) is not None:
+ # We have a filter set, let's see if theres anything to hide
+ # and remove from status
+ if (
+ torrent_id in self.status
+ and self.status[torrent_id]['state'] != state
+ ):
+ row[self.columns['filter'].column_indices[0]] = False
+ del self.status[torrent_id]
+
+ self.mark_dirty(torrent_id)
+
+ def on_sessionpaused_event(self):
+ self.mark_dirty()
+ self.update()
+
+ def on_sessionresumed_event(self):
+ self.mark_dirty()
+ self.update()
+
+ def on_torrentqueuechanged_event(self):
+ self.mark_dirty()
+ self.update()
+
+ # Handle keyboard shortcuts
+ def on_key_press_event(self, widget, event):
+ keyname = keyval_name(event.keyval)
+ if keyname is not None:
+ func = getattr(self, 'keypress_' + keyname.lower(), None)
+ if func:
+ return func(event)
+
+ def keypress_up(self, event):
+ """Handle any Up arrow keypresses"""
+ log.debug('keypress_up')
+ torrents = self.get_selected_torrents()
+ if not torrents:
+ return
+
+ # Move queue position up with Ctrl+Alt or Ctrl+Alt+Shift
+ if event.get_state() & CTRL_ALT_MASK:
+ if event.get_state() & ModifierType.SHIFT_MASK:
+ client.core.queue_top(torrents)
+ else:
+ client.core.queue_up(torrents)
+
+ def keypress_down(self, event):
+ """Handle any Down arrow keypresses"""
+ log.debug('keypress_down')
+ torrents = self.get_selected_torrents()
+ if not torrents:
+ return
+
+ # Move queue position down with Ctrl+Alt or Ctrl+Alt+Shift
+ if event.get_state() & CTRL_ALT_MASK:
+ if event.get_state() & ModifierType.SHIFT_MASK:
+ client.core.queue_bottom(torrents)
+ else:
+ client.core.queue_down(torrents)
+
+ def keypress_delete(self, event):
+ log.debug('keypress_delete')
+ torrents = self.get_selected_torrents()
+ if torrents:
+ if event.get_state() & ModifierType.SHIFT_MASK:
+ RemoveTorrentDialog(torrents, delete_files=True).run()
+ else:
+ RemoveTorrentDialog(torrents).run()
+
+ def keypress_menu(self, event):
+ log.debug('keypress_menu')
+ if not self.get_selected_torrent():
+ return
+
+ torrentmenu = component.get('MenuBar').torrentmenu
+ torrentmenu.popup(None, None, None, 3, event.time)
+ return True