1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com> 4# 5# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with 6# the additional special exception to link portions of this program with the OpenSSL library. 7# See LICENSE for more details. 8# 9 10"""The torrent view component that lists all torrents in the session.""" 11from __future__ import unicode_literals 12 13import logging 14from locale import strcoll 15 16from gi.repository.Gdk import ModifierType, keyval_name 17from gi.repository.GLib import idle_add 18from gi.repository.GObject import TYPE_UINT64 19from gi.repository.Gtk import EntryIconPosition 20from twisted.internet import reactor 21 22import deluge.component as component 23from deluge.common import decode_bytes 24from deluge.ui.client import client 25 26from . import torrentview_data_funcs as funcs 27from .common import cmp 28from .listview import ListView 29from .removetorrentdialog import RemoveTorrentDialog 30 31log = logging.getLogger(__name__) 32 33try: 34 CTRL_ALT_MASK = ModifierType.CONTROL_MASK | ModifierType.MOD1_MASK 35except TypeError: 36 # Sphinx AutoDoc has a mock issue with Gdk masks. 37 pass 38 39 40def str_nocase_sort(model, iter1, iter2, data): 41 """Sort string column data using ISO 14651 in lowercase. 42 43 Uses locale.strcoll which (allegedly) uses ISO 14651. Compares first 44 value with second and returns -1, 0, 1 for where it should be placed. 45 46 """ 47 v1 = model[iter1][data] 48 v2 = model[iter2][data] 49 # Catch any values of None from model. 50 v1 = v1.lower() if v1 else '' 51 v2 = v2.lower() if v2 else '' 52 return strcoll(v1, v2) 53 54 55def queue_peer_seed_sort_function(v1, v2): 56 if v1 == v2: 57 return 0 58 if v2 < 0: 59 return -1 60 if v1 < 0: 61 return 1 62 if v1 > v2: 63 return 1 64 if v2 > v1: 65 return -1 66 67 68def queue_column_sort(model, iter1, iter2, data): 69 v1 = model[iter1][data] 70 v2 = model[iter2][data] 71 return queue_peer_seed_sort_function(v1, v2) 72 73 74def eta_column_sort(model, iter1, iter2, data): 75 v1 = model[iter1][data] 76 v2 = model[iter2][data] 77 if v1 == v2: 78 return 0 79 if v1 == 0: 80 return 1 81 if v2 == 0: 82 return -1 83 if v1 > v2: 84 return 1 85 if v2 > v1: 86 return -1 87 88 89def seed_peer_column_sort(model, iter1, iter2, data): 90 v1 = model[iter1][data] # num seeds/peers 91 v3 = model[iter2][data] # num seeds/peers 92 if v1 == v3: 93 v2 = model[iter1][data + 1] # total seeds/peers 94 v4 = model[iter2][data + 1] # total seeds/peers 95 return queue_peer_seed_sort_function(v2, v4) 96 return queue_peer_seed_sort_function(v1, v3) 97 98 99def progress_sort(model, iter1, iter2, sort_column_id): 100 progress1 = model[iter1][sort_column_id] 101 progress2 = model[iter2][sort_column_id] 102 # Progress value is equal, so sort on state 103 if progress1 == progress2: 104 state1 = model[iter1][sort_column_id + 1] 105 state2 = model[iter2][sort_column_id + 1] 106 return cmp(state1, state2) 107 return cmp(progress1, progress2) 108 109 110class SearchBox(object): 111 def __init__(self, torrentview): 112 self.torrentview = torrentview 113 mainwindow = component.get('MainWindow') 114 main_builder = mainwindow.get_builder() 115 116 self.visible = False 117 self.search_pending = self.prefiltered = None 118 119 self.search_box = main_builder.get_object('search_box') 120 self.search_torrents_entry = main_builder.get_object('search_torrents_entry') 121 self.close_search_button = main_builder.get_object('close_search_button') 122 self.match_search_button = main_builder.get_object('search_torrents_match') 123 mainwindow.connect_signals(self) 124 125 def show(self): 126 self.visible = True 127 self.search_box.show_all() 128 self.search_torrents_entry.grab_focus() 129 130 def hide(self): 131 self.visible = False 132 self.clear_search() 133 self.search_box.hide() 134 self.search_pending = self.prefiltered = None 135 136 def clear_search(self): 137 if self.search_pending and self.search_pending.active(): 138 self.search_pending.cancel() 139 140 if self.prefiltered: 141 filter_column = self.torrentview.columns['filter'].column_indices[0] 142 torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0] 143 for row in self.torrentview.liststore: 144 torrent_id = row[torrent_id_column] 145 146 if torrent_id in self.prefiltered: 147 # Reset to previous filter state 148 self.prefiltered.pop(self.prefiltered.index(torrent_id)) 149 row[filter_column] = not row[filter_column] 150 151 self.prefiltered = None 152 153 self.search_torrents_entry.set_text('') 154 if self.torrentview.filter and 'name' in self.torrentview.filter: 155 self.torrentview.filter.pop('name', None) 156 self.search_pending = reactor.callLater(0.5, self.torrentview.update) 157 158 def set_search_filter(self): 159 if self.search_pending and self.search_pending.active(): 160 self.search_pending.cancel() 161 162 if self.torrentview.filter and 'name' in self.torrentview.filter: 163 self.torrentview.filter.pop('name', None) 164 165 elif self.torrentview.filter is None: 166 self.torrentview.filter = {} 167 168 search_string = self.search_torrents_entry.get_text() 169 if not search_string: 170 self.clear_search() 171 else: 172 if self.match_search_button.get_active(): 173 search_string += '::match' 174 self.torrentview.filter['name'] = search_string 175 self.prefilter_torrentview() 176 177 def prefilter_torrentview(self): 178 filter_column = self.torrentview.columns['filter'].column_indices[0] 179 torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0] 180 torrent_name_column = self.torrentview.columns[_('Name')].column_indices[1] 181 182 match_case = self.match_search_button.get_active() 183 if match_case: 184 search_string = self.search_torrents_entry.get_text() 185 else: 186 search_string = self.search_torrents_entry.get_text().lower() 187 188 if self.prefiltered is None: 189 self.prefiltered = [] 190 191 for row in self.torrentview.liststore: 192 torrent_id = row[torrent_id_column] 193 194 if torrent_id in self.prefiltered: 195 # Reset to previous filter state 196 self.prefiltered.pop(self.prefiltered.index(torrent_id)) 197 row[filter_column] = not row[filter_column] 198 199 if not row[filter_column]: 200 # Row is not visible(filtered out, but not by our filter), skip it 201 continue 202 203 if match_case: 204 torrent_name = row[torrent_name_column] 205 else: 206 torrent_name = row[torrent_name_column].lower() 207 208 if search_string in torrent_name and not row[filter_column]: 209 row[filter_column] = True 210 self.prefiltered.append(torrent_id) 211 elif search_string not in torrent_name and row[filter_column]: 212 row[filter_column] = False 213 self.prefiltered.append(torrent_id) 214 215 def on_close_search_button_clicked(self, widget): 216 self.hide() 217 218 def on_search_filter_toggle(self, widget): 219 if self.visible: 220 self.hide() 221 else: 222 self.show() 223 224 def on_search_torrents_match_toggled(self, widget): 225 if self.search_torrents_entry.get_text(): 226 self.set_search_filter() 227 self.search_pending = reactor.callLater(0.7, self.torrentview.update) 228 229 def on_search_torrents_entry_icon_press(self, entry, icon, event): 230 if icon != EntryIconPosition.SECONDARY: 231 return 232 self.clear_search() 233 234 def on_search_torrents_entry_changed(self, widget): 235 self.set_search_filter() 236 self.search_pending = reactor.callLater(0.7, self.torrentview.update) 237 238 239class TorrentView(ListView, component.Component): 240 """TorrentView handles the listing of torrents.""" 241 242 def __init__(self): 243 component.Component.__init__( 244 self, 'TorrentView', interval=2, depend=['SessionProxy'] 245 ) 246 main_builder = component.get('MainWindow').get_builder() 247 # Call the ListView constructor 248 ListView.__init__( 249 self, main_builder.get_object('torrent_view'), 'torrentview.state' 250 ) 251 log.debug('TorrentView Init..') 252 253 # If we have gotten the state yet 254 self.got_state = False 255 256 # This is where status updates are put 257 self.status = {} 258 259 # We keep a copy of the previous status to compare for changes 260 self.prev_status = {} 261 262 # Register the columns menu with the listview so it gets updated accordingly. 263 self.register_checklist_menu(main_builder.get_object('menu_columns')) 264 265 # Add the columns to the listview 266 self.add_text_column('torrent_id', hidden=True, unique=True) 267 self.add_bool_column('dirty', hidden=True) 268 self.add_func_column( 269 '#', 270 funcs.cell_data_queue, 271 [int], 272 status_field=['queue'], 273 sort_func=queue_column_sort, 274 ) 275 self.add_texticon_column( 276 _('Name'), 277 status_field=['state', 'name'], 278 function=funcs.cell_data_statusicon, 279 sort_func=str_nocase_sort, 280 default_sort=True, 281 ) 282 self.add_func_column( 283 _('Size'), 284 funcs.cell_data_size, 285 [TYPE_UINT64], 286 status_field=['total_wanted'], 287 ) 288 self.add_func_column( 289 _('Downloaded'), 290 funcs.cell_data_size, 291 [TYPE_UINT64], 292 status_field=['all_time_download'], 293 default=False, 294 ) 295 self.add_func_column( 296 _('Uploaded'), 297 funcs.cell_data_size, 298 [TYPE_UINT64], 299 status_field=['total_uploaded'], 300 default=False, 301 ) 302 self.add_func_column( 303 _('Remaining'), 304 funcs.cell_data_size, 305 [TYPE_UINT64], 306 status_field=['total_remaining'], 307 default=False, 308 ) 309 self.add_progress_column( 310 _('Progress'), 311 status_field=['progress', 'state'], 312 col_types=[float, str], 313 function=funcs.cell_data_progress, 314 sort_func=progress_sort, 315 ) 316 self.add_func_column( 317 _('Seeds'), 318 funcs.cell_data_peer, 319 [int, int], 320 status_field=['num_seeds', 'total_seeds'], 321 sort_func=seed_peer_column_sort, 322 default=False, 323 ) 324 self.add_func_column( 325 _('Peers'), 326 funcs.cell_data_peer, 327 [int, int], 328 status_field=['num_peers', 'total_peers'], 329 sort_func=seed_peer_column_sort, 330 default=False, 331 ) 332 self.add_func_column( 333 _('Seeds:Peers'), 334 funcs.cell_data_ratio_seeds_peers, 335 [float], 336 status_field=['seeds_peers_ratio'], 337 default=False, 338 ) 339 self.add_func_column( 340 _('Down Speed'), 341 funcs.cell_data_speed_down, 342 [int], 343 status_field=['download_payload_rate'], 344 ) 345 self.add_func_column( 346 _('Up Speed'), 347 funcs.cell_data_speed_up, 348 [int], 349 status_field=['upload_payload_rate'], 350 ) 351 self.add_func_column( 352 _('Down Limit'), 353 funcs.cell_data_speed_limit_down, 354 [float], 355 status_field=['max_download_speed'], 356 default=False, 357 ) 358 self.add_func_column( 359 _('Up Limit'), 360 funcs.cell_data_speed_limit_up, 361 [float], 362 status_field=['max_upload_speed'], 363 default=False, 364 ) 365 self.add_func_column( 366 _('ETA'), 367 funcs.cell_data_time, 368 [int], 369 status_field=['eta'], 370 sort_func=eta_column_sort, 371 ) 372 self.add_func_column( 373 _('Ratio'), 374 funcs.cell_data_ratio_ratio, 375 [float], 376 status_field=['ratio'], 377 default=False, 378 ) 379 self.add_func_column( 380 _('Avail'), 381 funcs.cell_data_ratio_avail, 382 [float], 383 status_field=['distributed_copies'], 384 default=False, 385 ) 386 self.add_func_column( 387 _('Added'), 388 funcs.cell_data_date_added, 389 [int], 390 status_field=['time_added'], 391 default=False, 392 ) 393 self.add_func_column( 394 _('Completed'), 395 funcs.cell_data_date_completed, 396 [int], 397 status_field=['completed_time'], 398 default=False, 399 ) 400 self.add_func_column( 401 _('Complete Seen'), 402 funcs.cell_data_date_or_never, 403 [int], 404 status_field=['last_seen_complete'], 405 default=False, 406 ) 407 self.add_texticon_column( 408 _('Tracker'), 409 function=funcs.cell_data_trackericon, 410 status_field=['tracker_host', 'tracker_host'], 411 default=False, 412 ) 413 self.add_text_column( 414 _('Download Folder'), status_field=['download_location'], default=False 415 ) 416 self.add_text_column(_('Owner'), status_field=['owner'], default=False) 417 self.add_bool_column( 418 _('Shared'), 419 status_field=['shared'], 420 default=False, 421 tooltip=_('Torrent is shared between other Deluge users or not.'), 422 ) 423 self.restore_columns_order_from_state() 424 425 # Set filter to None for now 426 self.filter = None 427 428 # Connect Signals # 429 # Connect to the 'button-press-event' to know when to bring up the 430 # torrent menu popup. 431 self.treeview.connect('button-press-event', self.on_button_press_event) 432 # Connect to the 'key-press-event' to know when the bring up the 433 # torrent menu popup via keypress. 434 self.treeview.connect('key-release-event', self.on_key_press_event) 435 # Connect to the 'changed' event of TreeViewSelection to get selection 436 # changes. 437 self.treeview.get_selection().connect('changed', self.on_selection_changed) 438 439 self.treeview.connect('drag-drop', self.on_drag_drop) 440 self.treeview.connect('drag_data_received', self.on_drag_data_received) 441 self.treeview.connect('key-press-event', self.on_key_press_event) 442 self.treeview.connect('columns-changed', self.on_columns_changed_event) 443 444 self.search_box = SearchBox(self) 445 self.permanent_status_keys = ['owner'] 446 self.columns_to_update = [] 447 448 def start(self): 449 """Start the torrentview""" 450 # We need to get the core session state to know which torrents are in 451 # the session so we can add them to our list. 452 # Only get the status fields required for the visible columns 453 status_fields = [] 454 for listview_column in self.columns.values(): 455 if listview_column.column.get_visible(): 456 if not listview_column.status_field: 457 continue 458 status_fields.extend(listview_column.status_field) 459 component.get('SessionProxy').get_torrents_status( 460 {}, status_fields 461 ).addCallback(self._on_session_state) 462 463 client.register_event_handler( 464 'TorrentStateChangedEvent', self.on_torrentstatechanged_event 465 ) 466 client.register_event_handler('TorrentAddedEvent', self.on_torrentadded_event) 467 client.register_event_handler( 468 'TorrentRemovedEvent', self.on_torrentremoved_event 469 ) 470 client.register_event_handler('SessionPausedEvent', self.on_sessionpaused_event) 471 client.register_event_handler( 472 'SessionResumedEvent', self.on_sessionresumed_event 473 ) 474 client.register_event_handler( 475 'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event 476 ) 477 478 def _on_session_state(self, state): 479 self.add_rows(state) 480 self.got_state = True 481 # Update the view right away with our status 482 self.status = state 483 self.set_columns_to_update() 484 self.update_view(load_new_list=True) 485 self.select_first_row() 486 487 def stop(self): 488 """Stops the torrentview""" 489 client.deregister_event_handler( 490 'TorrentStateChangedEvent', self.on_torrentstatechanged_event 491 ) 492 client.deregister_event_handler('TorrentAddedEvent', self.on_torrentadded_event) 493 client.deregister_event_handler( 494 'TorrentRemovedEvent', self.on_torrentremoved_event 495 ) 496 client.deregister_event_handler( 497 'SessionPausedEvent', self.on_sessionpaused_event 498 ) 499 client.deregister_event_handler( 500 'SessionResumedEvent', self.on_sessionresumed_event 501 ) 502 client.deregister_event_handler( 503 'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event 504 ) 505 506 if self.treeview.get_selection(): 507 self.treeview.get_selection().unselect_all() 508 509 # Save column state before clearing liststore 510 # so column sort details are correctly saved. 511 self.save_state() 512 self.liststore.clear() 513 self.prev_status = {} 514 self.filter = None 515 self.search_box.hide() 516 517 def shutdown(self): 518 """Called when GtkUi is exiting""" 519 pass 520 521 def save_state(self): 522 """ 523 Saves the state of the torrent view. 524 """ 525 if component.get('MainWindow').visible(): 526 ListView.save_state(self, 'torrentview.state') 527 528 def remove_column(self, header): 529 """Removes the column with the name 'header' from the torrentview""" 530 self.save_state() 531 ListView.remove_column(self, header) 532 533 def set_filter(self, filter_dict): 534 """ 535 Sets filters for the torrentview.. 536 537 see: core.get_torrents_status 538 """ 539 search_filter = self.filter and self.filter.get('name', None) or None 540 self.filter = dict(filter_dict) # Copied version of filter_dict. 541 if search_filter and 'name' not in filter_dict: 542 self.filter['name'] = search_filter 543 self.update(select_row=True) 544 545 def set_columns_to_update(self, columns=None): 546 status_keys = [] 547 self.columns_to_update = [] 548 549 if columns is None: 550 # We need to iterate through all columns 551 columns = list(self.columns) 552 553 # Iterate through supplied list of columns to update 554 for column in columns: 555 # Make sure column is visible and has 'status_field' set. 556 # If not, we can ignore it. 557 if ( 558 self.columns[column].column.get_visible() is True 559 and self.columns[column].hidden is False 560 and self.columns[column].status_field is not None 561 ): 562 for field in self.columns[column].status_field: 563 status_keys.append(field) 564 self.columns_to_update.append(column) 565 566 # Remove duplicates 567 self.columns_to_update = list(set(self.columns_to_update)) 568 status_keys = list(set(status_keys + self.permanent_status_keys)) 569 return status_keys 570 571 def send_status_request(self, columns=None, select_row=False): 572 # Store the 'status_fields' we need to send to core 573 status_keys = self.set_columns_to_update(columns) 574 575 # If there is nothing in status_keys then we must not continue 576 if status_keys is []: 577 return 578 579 # Remove duplicates from status_key list 580 status_keys = list(set(status_keys)) 581 582 # Request the statuses for all these torrent_ids, this is async so we 583 # will deal with the return in a signal callback. 584 d = ( 585 component.get('SessionProxy') 586 .get_torrents_status(self.filter, status_keys) 587 .addCallback(self._on_get_torrents_status) 588 ) 589 if select_row: 590 d.addCallback(self.select_first_row) 591 592 def select_first_row(self, ignored=None): 593 """ 594 Set the first row in the list selected if a selection does 595 not already exist 596 """ 597 rows = self.treeview.get_selection().get_selected_rows()[1] 598 # Only select row if noe rows are selected 599 if not rows: 600 self.treeview.get_selection().select_path((0,)) 601 602 def update(self, select_row=False): 603 """ 604 Sends a status request to core and updates the torrent list with the result. 605 606 :param select_row: if the first row in the list should be selected if 607 no rows are already selected. 608 :type select_row: boolean 609 610 """ 611 if self.got_state: 612 if ( 613 self.search_box.search_pending is not None 614 and self.search_box.search_pending.active() 615 ): 616 # An update request is scheduled, let's wait for that one 617 return 618 # Send a status request 619 idle_add(self.send_status_request, None, select_row) 620 621 def update_view(self, load_new_list=False): 622 """Update the torrent view model with data we've received.""" 623 filter_column = self.columns['filter'].column_indices[0] 624 status = self.status 625 626 if not load_new_list: 627 # Freeze notications while updating 628 self.treeview.freeze_child_notify() 629 630 # Get the columns to update from one of the torrents 631 if status: 632 torrent_id = list(status)[0] 633 fields_to_update = [] 634 for column in self.columns_to_update: 635 column_index = self.get_column_index(column) 636 for i, status_field in enumerate(self.columns[column].status_field): 637 # Only use columns that the torrent has in the state 638 if status_field in status[torrent_id]: 639 fields_to_update.append((column_index[i], status_field)) 640 641 for row in self.liststore: 642 torrent_id = row[self.columns['torrent_id'].column_indices[0]] 643 # We expect the torrent_id to be in status and prev_status, 644 # as it will be as long as the list isn't changed by the user 645 646 torrent_id_in_status = False 647 try: 648 torrent_status = status[torrent_id] 649 torrent_id_in_status = True 650 if torrent_status == self.prev_status[torrent_id]: 651 # The status dict is the same, so do nothing to update for this torrent 652 continue 653 except KeyError: 654 pass 655 656 if not torrent_id_in_status: 657 if row[filter_column] is True: 658 row[filter_column] = False 659 else: 660 if row[filter_column] is False: 661 row[filter_column] = True 662 663 # Find the fields to update 664 to_update = [] 665 for i, status_field in fields_to_update: 666 row_value = status[torrent_id][status_field] 667 if decode_bytes(row[i]) != row_value: 668 to_update.append(i) 669 to_update.append(row_value) 670 # Update fields in the liststore 671 if to_update: 672 self.liststore.set(row.iter, *to_update) 673 674 if load_new_list: 675 # Create the model filter. This sets the model for the treeview and enables sorting. 676 self.create_model_filter() 677 else: 678 self.treeview.thaw_child_notify() 679 680 component.get('MenuBar').update_menu() 681 self.prev_status = status 682 683 def _on_get_torrents_status(self, status, select_row=False): 684 """Callback function for get_torrents_status(). 'status' should be a 685 dictionary of {torrent_id: {key, value}}.""" 686 self.status = status 687 if self.search_box.prefiltered is not None: 688 self.search_box.prefiltered = None 689 690 if self.status == self.prev_status and self.prev_status: 691 # We do not bother updating since the status hasn't changed 692 self.prev_status = self.status 693 return 694 self.update_view() 695 696 def add_rows(self, torrent_ids): 697 """Accepts a list of torrent_ids to add to self.liststore""" 698 torrent_id_column = self.columns['torrent_id'].column_indices[0] 699 dirty_column = self.columns['dirty'].column_indices[0] 700 filter_column = self.columns['filter'].column_indices[0] 701 for torrent_id in torrent_ids: 702 # Insert a new row to the liststore 703 row = self.liststore.append() 704 self.liststore.set( 705 row, 706 torrent_id_column, 707 torrent_id, 708 dirty_column, 709 True, 710 filter_column, 711 True, 712 ) 713 714 def remove_row(self, torrent_id): 715 """Removes a row with torrent_id""" 716 for row in self.liststore: 717 if row[self.columns['torrent_id'].column_indices[0]] == torrent_id: 718 self.liststore.remove(row.iter) 719 # Force an update of the torrentview 720 self.update(select_row=True) 721 break 722 723 def mark_dirty(self, torrent_id=None): 724 for row in self.liststore: 725 if ( 726 not torrent_id 727 or row[self.columns['torrent_id'].column_indices[0]] == torrent_id 728 ): 729 # log.debug('marking %s dirty', torrent_id) 730 row[self.columns['dirty'].column_indices[0]] = True 731 if torrent_id: 732 break 733 734 def get_selected_torrent(self): 735 """Returns a torrent_id or None. If multiple torrents are selected, 736 it will return the torrent_id of the first one.""" 737 selected = self.get_selected_torrents() 738 if selected: 739 return selected[0] 740 else: 741 return selected 742 743 def get_selected_torrents(self): 744 """Returns a list of selected torrents or None""" 745 torrent_ids = [] 746 try: 747 paths = self.treeview.get_selection().get_selected_rows()[1] 748 except AttributeError: 749 # paths is likely None .. so lets return [] 750 return [] 751 try: 752 for path in paths: 753 try: 754 row = self.treeview.get_model().get_iter(path) 755 except Exception as ex: 756 log.debug('Unable to get iter from path: %s', ex) 757 continue 758 759 child_row = self.treeview.get_model().convert_iter_to_child_iter(row) 760 child_row = ( 761 self.treeview.get_model() 762 .get_model() 763 .convert_iter_to_child_iter(child_row) 764 ) 765 if self.liststore.iter_is_valid(child_row): 766 try: 767 value = self.liststore.get_value( 768 child_row, self.columns['torrent_id'].column_indices[0] 769 ) 770 except Exception as ex: 771 log.debug('Unable to get value from row: %s', ex) 772 else: 773 torrent_ids.append(value) 774 if len(torrent_ids) == 0: 775 return [] 776 777 return torrent_ids 778 except (ValueError, TypeError): 779 return [] 780 781 def get_torrent_status(self, torrent_id): 782 """Returns data stored in self.status, it may not be complete""" 783 try: 784 return self.status[torrent_id] 785 except KeyError: 786 return {} 787 788 def get_visible_torrents(self): 789 return list(self.status) 790 791 # Callbacks # 792 def on_button_press_event(self, widget, event): 793 """This is a callback for showing the right-click context menu.""" 794 log.debug('on_button_press_event') 795 # We only care about right-clicks 796 if event.button == 3 and event.window == self.treeview.get_bin_window(): 797 x, y = event.get_coords() 798 path = self.treeview.get_path_at_pos(int(x), int(y)) 799 if not path: 800 return 801 row = self.model_filter.get_iter(path[0]) 802 803 if self.get_selected_torrents(): 804 if ( 805 self.model_filter.get_value( 806 row, self.columns['torrent_id'].column_indices[0] 807 ) 808 not in self.get_selected_torrents() 809 ): 810 self.treeview.get_selection().unselect_all() 811 self.treeview.get_selection().select_iter(row) 812 else: 813 self.treeview.get_selection().select_iter(row) 814 torrentmenu = component.get('MenuBar').torrentmenu 815 torrentmenu.popup(None, None, None, None, event.button, event.time) 816 return True 817 818 def on_selection_changed(self, treeselection): 819 """This callback is know when the selection has changed.""" 820 log.debug('on_selection_changed') 821 component.get('TorrentDetails').update() 822 component.get('MenuBar').update_menu() 823 824 def on_drag_drop(self, widget, drag_context, x, y, timestamp): 825 widget.stop_emission('drag-drop') 826 827 def on_drag_data_received( 828 self, widget, drag_context, x, y, selection_data, info, timestamp 829 ): 830 widget.stop_emission('drag_data_received') 831 832 def on_columns_changed_event(self, treeview): 833 log.debug('Treeview Columns Changed') 834 self.save_state() 835 836 def on_torrentadded_event(self, torrent_id, from_state): 837 self.add_rows([torrent_id]) 838 self.update(select_row=True) 839 840 def on_torrentremoved_event(self, torrent_id): 841 self.remove_row(torrent_id) 842 843 def on_torrentstatechanged_event(self, torrent_id, state): 844 # Update the torrents state 845 for row in self.liststore: 846 if torrent_id != row[self.columns['torrent_id'].column_indices[0]]: 847 continue 848 849 for name in self.columns_to_update: 850 if not self.columns[name].status_field: 851 continue 852 for idx, status_field in enumerate(self.columns[name].status_field): 853 # Update all columns that use the state field to current state 854 if status_field != 'state': 855 continue 856 row[self.get_column_index(name)[idx]] = state 857 858 if self.filter.get('state', None) is not None: 859 # We have a filter set, let's see if theres anything to hide 860 # and remove from status 861 if ( 862 torrent_id in self.status 863 and self.status[torrent_id]['state'] != state 864 ): 865 row[self.columns['filter'].column_indices[0]] = False 866 del self.status[torrent_id] 867 868 self.mark_dirty(torrent_id) 869 870 def on_sessionpaused_event(self): 871 self.mark_dirty() 872 self.update() 873 874 def on_sessionresumed_event(self): 875 self.mark_dirty() 876 self.update(select_row=True) 877 878 def on_torrentqueuechanged_event(self): 879 self.mark_dirty() 880 self.update() 881 882 # Handle keyboard shortcuts 883 def on_key_press_event(self, widget, event): 884 keyname = keyval_name(event.keyval) 885 if keyname is not None: 886 func = getattr(self, 'keypress_' + keyname.lower(), None) 887 if func: 888 return func(event) 889 890 def keypress_up(self, event): 891 """Handle any Up arrow keypresses""" 892 log.debug('keypress_up') 893 torrents = self.get_selected_torrents() 894 if not torrents: 895 return 896 897 # Move queue position up with Ctrl+Alt or Ctrl+Alt+Shift 898 if event.get_state() & CTRL_ALT_MASK: 899 if event.get_state() & ModifierType.SHIFT_MASK: 900 client.core.queue_top(torrents) 901 else: 902 client.core.queue_up(torrents) 903 904 def keypress_down(self, event): 905 """Handle any Down arrow keypresses""" 906 log.debug('keypress_down') 907 torrents = self.get_selected_torrents() 908 if not torrents: 909 return 910 911 # Move queue position down with Ctrl+Alt or Ctrl+Alt+Shift 912 if event.get_state() & CTRL_ALT_MASK: 913 if event.get_state() & ModifierType.SHIFT_MASK: 914 client.core.queue_bottom(torrents) 915 else: 916 client.core.queue_down(torrents) 917 918 def keypress_delete(self, event): 919 log.debug('keypress_delete') 920 torrents = self.get_selected_torrents() 921 if torrents: 922 if event.get_state() & ModifierType.SHIFT_MASK: 923 RemoveTorrentDialog(torrents, delete_files=True).run() 924 else: 925 RemoveTorrentDialog(torrents).run() 926 927 def keypress_menu(self, event): 928 log.debug('keypress_menu') 929 if not self.get_selected_torrent(): 930 return 931 932 torrentmenu = component.get('MenuBar').torrentmenu 933 torrentmenu.popup(None, None, None, None, 3, event.time) 934 return True 935