deluge/deluge/ui/gtk3/torrentview.py
Calum Lind 70d5931622 [GTK3] Fix column header right-click menu popup
The popup_at_pointer method is only available in GTK >=3.22 so for
compatibility restore using popup method.

Right-clicking on column headers popped-up torrent menu so only show
this menu when in torrentview.
2018-11-02 08:45:39 +00:00

934 lines
33 KiB
Python

# -*- 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.GLib import idle_add
from gi.repository.GObject import TYPE_UINT64
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 .common import cmp
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 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]
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 and event.window == self.treeview.get_bin_window():
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