1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com> 4# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> 5# 6# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with 7# the additional special exception to link portions of this program with the OpenSSL library. 8# See LICENSE for more details. 9# 10 11 12from __future__ import unicode_literals 13 14import logging 15import os.path 16 17from gi.repository import Gtk 18 19import deluge.common 20import deluge.component as component 21from deluge.configmanager import ConfigManager 22from deluge.ui.client import client 23 24from .dialogs import ErrorDialog, OtherDialog 25from .path_chooser import PathChooser 26 27log = logging.getLogger(__name__) 28 29 30class MenuBar(component.Component): 31 def __init__(self): 32 log.debug('MenuBar init..') 33 component.Component.__init__(self, 'MenuBar') 34 self.mainwindow = component.get('MainWindow') 35 self.main_builder = self.mainwindow.get_builder() 36 self.config = ConfigManager('gtk3ui.conf') 37 38 self.builder = Gtk.Builder() 39 # Get the torrent menu from the gtk builder file 40 self.builder.add_from_file( 41 deluge.common.resource_filename( 42 __package__, os.path.join('glade', 'torrent_menu.ui') 43 ) 44 ) 45 # Get the torrent options menu from the gtk builder file 46 self.builder.add_from_file( 47 deluge.common.resource_filename( 48 __package__, os.path.join('glade', 'torrent_menu.options.ui') 49 ) 50 ) 51 # Get the torrent queue menu from the gtk builder file 52 self.builder.add_from_file( 53 deluge.common.resource_filename( 54 __package__, os.path.join('glade', 'torrent_menu.queue.ui') 55 ) 56 ) 57 58 # Attach queue torrent menu 59 torrent_queue_menu = self.builder.get_object('queue_torrent_menu') 60 self.builder.get_object('menuitem_queue').set_submenu(torrent_queue_menu) 61 # Attach options torrent menu 62 torrent_options_menu = self.builder.get_object('options_torrent_menu') 63 self.builder.get_object('menuitem_options').set_submenu(torrent_options_menu) 64 65 self.builder.get_object('download-limit-image').set_from_file( 66 deluge.common.get_pixmap('downloading16.png') 67 ) 68 self.builder.get_object('upload-limit-image').set_from_file( 69 deluge.common.get_pixmap('seeding16.png') 70 ) 71 72 for menuitem in ( 73 'menuitem_down_speed', 74 'menuitem_up_speed', 75 'menuitem_max_connections', 76 'menuitem_upload_slots', 77 ): 78 submenu = Gtk.Menu() 79 item = Gtk.MenuItem.new_with_label(_('Set Unlimited')) 80 item.set_name(menuitem) 81 item.connect('activate', self.on_menuitem_set_unlimited) 82 submenu.append(item) 83 item = Gtk.MenuItem.new_with_label(_('Other...')) 84 item.set_name(menuitem) 85 item.connect('activate', self.on_menuitem_set_other) 86 submenu.append(item) 87 submenu.show_all() 88 self.builder.get_object(menuitem).set_submenu(submenu) 89 90 submenu = Gtk.Menu() 91 item = Gtk.MenuItem.new_with_label(_('On')) 92 item.connect('activate', self.on_menuitem_set_automanaged_on) 93 submenu.append(item) 94 item = Gtk.MenuItem.new_with_label(_('Off')) 95 item.connect('activate', self.on_menuitem_set_automanaged_off) 96 submenu.append(item) 97 submenu.show_all() 98 self.builder.get_object('menuitem_auto_managed').set_submenu(submenu) 99 100 submenu = Gtk.Menu() 101 item = Gtk.MenuItem.new_with_label(_('Disable')) 102 item.connect('activate', self.on_menuitem_set_stop_seed_at_ratio_disable) 103 submenu.append(item) 104 item = Gtk.MenuItem.new_with_label(_('Enable...')) 105 item.set_name('menuitem_stop_seed_at_ratio') 106 item.connect('activate', self.on_menuitem_set_other) 107 submenu.append(item) 108 submenu.show_all() 109 self.builder.get_object('menuitem_stop_seed_at_ratio').set_submenu(submenu) 110 111 self.torrentmenu = self.builder.get_object('torrent_menu') 112 self.menu_torrent = self.main_builder.get_object('menu_torrent') 113 114 # Attach the torrent_menu to the Torrent file menu 115 self.menu_torrent.set_submenu(self.torrentmenu) 116 117 # Make sure the view menuitems are showing the correct active state 118 self.main_builder.get_object('menuitem_toolbar').set_active( 119 self.config['show_toolbar'] 120 ) 121 self.main_builder.get_object('menuitem_sidebar').set_active( 122 self.config['show_sidebar'] 123 ) 124 self.main_builder.get_object('menuitem_statusbar').set_active( 125 self.config['show_statusbar'] 126 ) 127 self.main_builder.get_object('sidebar_show_zero').set_active( 128 self.config['sidebar_show_zero'] 129 ) 130 self.main_builder.get_object('sidebar_show_trackers').set_active( 131 self.config['sidebar_show_trackers'] 132 ) 133 self.main_builder.get_object('sidebar_show_owners').set_active( 134 self.config['sidebar_show_owners'] 135 ) 136 137 # Connect main window Signals # 138 self.mainwindow.connect_signals(self) 139 140 # Connect menubar signals 141 self.builder.connect_signals(self) 142 143 self.change_sensitivity = ['menuitem_addtorrent'] 144 145 def start(self): 146 for widget in self.change_sensitivity: 147 self.main_builder.get_object(widget).set_sensitive(True) 148 149 # Only show open_folder menuitem and separator if connected to a localhost daemon. 150 localhost_items = ['menuitem_open_folder', 'separator4'] 151 if client.is_localhost(): 152 for widget in localhost_items: 153 self.builder.get_object(widget).show() 154 self.builder.get_object(widget).set_no_show_all(False) 155 else: 156 for widget in localhost_items: 157 self.builder.get_object(widget).hide() 158 self.builder.get_object(widget).set_no_show_all(True) 159 160 self.main_builder.get_object('separatormenuitem').set_visible( 161 not self.config['standalone'] 162 ) 163 self.main_builder.get_object('menuitem_quitdaemon').set_visible( 164 not self.config['standalone'] 165 ) 166 self.main_builder.get_object('menuitem_connectionmanager').set_visible( 167 not self.config['standalone'] 168 ) 169 170 # Show the Torrent menu because we're connected to a host 171 self.menu_torrent.show() 172 173 if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN: 174 # Get known accounts to allow changing ownership 175 client.core.get_known_accounts().addCallback( 176 self._on_known_accounts 177 ).addErrback(self._on_known_accounts_fail) 178 179 client.register_event_handler( 180 'TorrentStateChangedEvent', self.on_torrentstatechanged_event 181 ) 182 client.register_event_handler( 183 'TorrentResumedEvent', self.on_torrentresumed_event 184 ) 185 client.register_event_handler('SessionPausedEvent', self.on_sessionpaused_event) 186 client.register_event_handler( 187 'SessionResumedEvent', self.on_sessionresumed_event 188 ) 189 190 def stop(self): 191 log.debug('MenuBar stopping') 192 193 client.deregister_event_handler( 194 'TorrentStateChangedEvent', self.on_torrentstatechanged_event 195 ) 196 client.deregister_event_handler( 197 'TorrentResumedEvent', self.on_torrentresumed_event 198 ) 199 client.deregister_event_handler( 200 'SessionPausedEvent', self.on_sessionpaused_event 201 ) 202 client.deregister_event_handler( 203 'SessionResumedEvent', self.on_sessionresumed_event 204 ) 205 206 for widget in self.change_sensitivity: 207 self.main_builder.get_object(widget).set_sensitive(False) 208 209 # Hide the Torrent menu 210 self.menu_torrent.hide() 211 212 self.main_builder.get_object('separatormenuitem').hide() 213 self.main_builder.get_object('menuitem_quitdaemon').hide() 214 215 def update_menu(self): 216 selected = component.get('TorrentView').get_selected_torrents() 217 if not selected or len(selected) == 0: 218 # No torrent is selected. Disable the 'Torrents' menu 219 self.menu_torrent.set_sensitive(False) 220 return 221 222 self.menu_torrent.set_sensitive(True) 223 # XXX: Should also update Pause/Resume/Remove menuitems. 224 # Any better way than duplicating toolbar.py:update_buttons in here? 225 226 def add_torrentmenu_separator(self): 227 sep = Gtk.SeparatorMenuItem() 228 self.torrentmenu.append(sep) 229 sep.show() 230 return sep 231 232 # Callbacks # 233 def on_torrentstatechanged_event(self, torrent_id, state): 234 if state == 'Paused': 235 self.update_menu() 236 237 def on_torrentresumed_event(self, torrent_id): 238 self.update_menu() 239 240 def on_sessionpaused_event(self): 241 self.update_menu() 242 243 def on_sessionresumed_event(self): 244 self.update_menu() 245 246 # File Menu # 247 def on_menuitem_addtorrent_activate(self, data=None): 248 log.debug('on_menuitem_addtorrent_activate') 249 component.get('AddTorrentDialog').show() 250 251 def on_menuitem_createtorrent_activate(self, data=None): 252 log.debug('on_menuitem_createtorrent_activate') 253 from .createtorrentdialog import CreateTorrentDialog 254 255 CreateTorrentDialog().show() 256 257 def on_menuitem_quitdaemon_activate(self, data=None): 258 log.debug('on_menuitem_quitdaemon_activate') 259 self.mainwindow.quit(shutdown=True) 260 261 def on_menuitem_quit_activate(self, data=None): 262 log.debug('on_menuitem_quit_activate') 263 self.mainwindow.quit() 264 265 # Edit Menu # 266 def on_menuitem_preferences_activate(self, data=None): 267 log.debug('on_menuitem_preferences_activate') 268 component.get('Preferences').show() 269 270 def on_menuitem_connectionmanager_activate(self, data=None): 271 log.debug('on_menuitem_connectionmanager_activate') 272 component.get('ConnectionManager').show() 273 274 # Torrent Menu # 275 def on_menuitem_pause_activate(self, data=None): 276 log.debug('on_menuitem_pause_activate') 277 client.core.pause_torrents(component.get('TorrentView').get_selected_torrents()) 278 279 def on_menuitem_resume_activate(self, data=None): 280 log.debug('on_menuitem_resume_activate') 281 client.core.resume_torrents( 282 component.get('TorrentView').get_selected_torrents() 283 ) 284 285 def on_menuitem_updatetracker_activate(self, data=None): 286 log.debug('on_menuitem_updatetracker_activate') 287 client.core.force_reannounce( 288 component.get('TorrentView').get_selected_torrents() 289 ) 290 291 def on_menuitem_edittrackers_activate(self, data=None): 292 log.debug('on_menuitem_edittrackers_activate') 293 from .edittrackersdialog import EditTrackersDialog 294 295 dialog = EditTrackersDialog( 296 component.get('TorrentView').get_selected_torrent(), self.mainwindow.window 297 ) 298 dialog.run() 299 300 def on_menuitem_remove_activate(self, data=None): 301 log.debug('on_menuitem_remove_activate') 302 torrent_ids = component.get('TorrentView').get_selected_torrents() 303 if torrent_ids: 304 from .removetorrentdialog import RemoveTorrentDialog 305 306 RemoveTorrentDialog(torrent_ids).run() 307 308 def on_menuitem_recheck_activate(self, data=None): 309 log.debug('on_menuitem_recheck_activate') 310 client.core.force_recheck(component.get('TorrentView').get_selected_torrents()) 311 312 def on_menuitem_open_folder_activate(self, data=None): 313 log.debug('on_menuitem_open_folder') 314 315 def _on_torrent_status(status): 316 timestamp = component.get('MainWindow').get_timestamp() 317 path = os.path.join( 318 status['download_location'], status['files'][0]['path'].split('/')[0] 319 ) 320 deluge.common.show_file(path, timestamp=timestamp) 321 322 for torrent_id in component.get('TorrentView').get_selected_torrents(): 323 component.get('SessionProxy').get_torrent_status( 324 torrent_id, ['download_location', 'files'] 325 ).addCallback(_on_torrent_status) 326 327 def on_menuitem_move_activate(self, data=None): 328 log.debug('on_menuitem_move_activate') 329 component.get('SessionProxy').get_torrent_status( 330 component.get('TorrentView').get_selected_torrent(), ['download_location'] 331 ).addCallback(self.show_move_storage_dialog) 332 333 def show_move_storage_dialog(self, status): 334 log.debug('show_move_storage_dialog') 335 builder = Gtk.Builder() 336 builder.add_from_file( 337 deluge.common.resource_filename( 338 __package__, os.path.join('glade', 'move_storage_dialog.ui') 339 ) 340 ) 341 # Keep it referenced: 342 # https://bugzilla.gnome.org/show_bug.cgi?id=546802 343 self.move_storage_dialog = builder.get_object('move_storage_dialog') 344 self.move_storage_dialog.set_transient_for(self.mainwindow.window) 345 self.move_storage_dialog_hbox = builder.get_object('hbox_entry') 346 self.move_storage_path_chooser = PathChooser( 347 'move_completed_paths_list', self.move_storage_dialog 348 ) 349 self.move_storage_dialog_hbox.add(self.move_storage_path_chooser) 350 self.move_storage_dialog_hbox.show_all() 351 self.move_storage_path_chooser.set_text(status['download_location']) 352 353 def on_dialog_response_event(widget, response_id): 354 def on_core_result(result): 355 # Delete references 356 self.move_storage_dialog.hide() 357 del self.move_storage_dialog 358 del self.move_storage_dialog_hbox 359 360 if response_id == Gtk.ResponseType.CANCEL: 361 on_core_result(None) 362 363 if response_id == Gtk.ResponseType.OK: 364 log.debug( 365 'Moving torrents to %s', self.move_storage_path_chooser.get_text() 366 ) 367 path = self.move_storage_path_chooser.get_text() 368 client.core.move_storage( 369 component.get('TorrentView').get_selected_torrents(), path 370 ).addCallback(on_core_result) 371 372 self.move_storage_dialog.connect('response', on_dialog_response_event) 373 self.move_storage_dialog.show() 374 375 def on_menuitem_queue_top_activate(self, value): 376 log.debug('on_menuitem_queue_top_activate') 377 client.core.queue_top(component.get('TorrentView').get_selected_torrents()) 378 379 def on_menuitem_queue_up_activate(self, value): 380 log.debug('on_menuitem_queue_up_activate') 381 client.core.queue_up(component.get('TorrentView').get_selected_torrents()) 382 383 def on_menuitem_queue_down_activate(self, value): 384 log.debug('on_menuitem_queue_down_activate') 385 client.core.queue_down(component.get('TorrentView').get_selected_torrents()) 386 387 def on_menuitem_queue_bottom_activate(self, value): 388 log.debug('on_menuitem_queue_bottom_activate') 389 client.core.queue_bottom(component.get('TorrentView').get_selected_torrents()) 390 391 # View Menu # 392 def on_menuitem_toolbar_toggled(self, value): 393 log.debug('on_menuitem_toolbar_toggled') 394 component.get('ToolBar').visible(value.get_active()) 395 396 def on_menuitem_sidebar_toggled(self, value): 397 log.debug('on_menuitem_sidebar_toggled') 398 component.get('SideBar').visible(value.get_active()) 399 400 def on_menuitem_statusbar_toggled(self, value): 401 log.debug('on_menuitem_statusbar_toggled') 402 component.get('StatusBar').visible(value.get_active()) 403 404 # Help Menu # 405 def on_menuitem_homepage_activate(self, data=None): 406 log.debug('on_menuitem_homepage_activate') 407 deluge.common.open_url_in_browser('http://deluge-torrent.org') 408 409 def on_menuitem_faq_activate(self, data=None): 410 log.debug('on_menuitem_faq_activate') 411 deluge.common.open_url_in_browser('http://dev.deluge-torrent.org/wiki/Faq') 412 413 def on_menuitem_community_activate(self, data=None): 414 log.debug('on_menuitem_community_activate') 415 deluge.common.open_url_in_browser('http://forum.deluge-torrent.org/') 416 417 def on_menuitem_about_activate(self, data=None): 418 log.debug('on_menuitem_about_activate') 419 from .aboutdialog import AboutDialog 420 421 AboutDialog().run() 422 423 def on_menuitem_set_unlimited(self, widget): 424 log.debug('widget name: %s', widget.get_name()) 425 funcs = { 426 'menuitem_down_speed': 'max_download_speed', 427 'menuitem_up_speed': 'max_upload_speed', 428 'menuitem_max_connections': 'max_connections', 429 'menuitem_upload_slots': 'max_upload_slots', 430 } 431 if widget.get_name() in funcs: 432 torrent_ids = component.get('TorrentView').get_selected_torrents() 433 client.core.set_torrent_options(torrent_ids, {funcs[widget.get_name()]: -1}) 434 435 def on_menuitem_set_other(self, widget): 436 log.debug('widget name: %s', widget.get_name()) 437 status_map = { 438 'menuitem_down_speed': ['max_download_speed', 'max_download_speed'], 439 'menuitem_up_speed': ['max_upload_speed', 'max_upload_speed'], 440 'menuitem_max_connections': ['max_connections', 'max_connections_global'], 441 'menuitem_upload_slots': ['max_upload_slots', 'max_upload_slots_global'], 442 'menuitem_stop_seed_at_ratio': ['stop_ratio', 'stop_seed_ratio'], 443 } 444 445 other_dialog_info = { 446 'menuitem_down_speed': [ 447 _('Download Speed Limit'), 448 _('Set the maximum download speed'), 449 _('KiB/s'), 450 'downloading.svg', 451 ], 452 'menuitem_up_speed': [ 453 _('Upload Speed Limit'), 454 _('Set the maximum upload speed'), 455 _('KiB/s'), 456 'seeding.svg', 457 ], 458 'menuitem_max_connections': [ 459 _('Incoming Connections'), 460 _('Set the maximum incoming connections'), 461 '', 462 'network-transmit-receive-symbolic', 463 ], 464 'menuitem_upload_slots': [ 465 _('Peer Upload Slots'), 466 _('Set the maximum upload slots'), 467 '', 468 'view-sort-descending-symbolic', 469 ], 470 'menuitem_stop_seed_at_ratio': [ 471 _('Stop Seed At Ratio'), 472 'Stop torrent seeding at ratio', 473 '', 474 None, 475 ], 476 } 477 478 core_key = status_map[widget.get_name()][0] 479 core_key_global = status_map[widget.get_name()][1] 480 481 def _on_torrent_status(status): 482 other_dialog = other_dialog_info[widget.get_name()] 483 # Add the default using status value 484 if status: 485 other_dialog.append(status[core_key_global]) 486 487 def set_value(value): 488 if value is not None: 489 if value == 0: 490 value += -1 491 options = {core_key: value} 492 if core_key == 'stop_ratio': 493 options['stop_at_ratio'] = True 494 client.core.set_torrent_options(torrent_ids, options) 495 496 dialog = OtherDialog(*other_dialog) 497 dialog.run().addCallback(set_value) 498 499 torrent_ids = component.get('TorrentView').get_selected_torrents() 500 if len(torrent_ids) == 1: 501 core_key_global = core_key 502 d = component.get('SessionProxy').get_torrent_status( 503 torrent_ids[0], [core_key] 504 ) 505 else: 506 d = client.core.get_config_values([core_key_global]) 507 d.addCallback(_on_torrent_status) 508 509 def on_menuitem_set_automanaged_on(self, widget): 510 client.core.set_torrent_options( 511 component.get('TorrentView').get_selected_torrents(), {'auto_managed': True} 512 ) 513 514 def on_menuitem_set_automanaged_off(self, widget): 515 client.core.set_torrent_options( 516 component.get('TorrentView').get_selected_torrents(), 517 {'auto_managed': False}, 518 ) 519 520 def on_menuitem_set_stop_seed_at_ratio_disable(self, widget): 521 client.core.set_torrent_options( 522 component.get('TorrentView').get_selected_torrents(), 523 {'stop_at_ratio': False}, 524 ) 525 526 def on_menuitem_sidebar_zero_toggled(self, widget): 527 self.config['sidebar_show_zero'] = widget.get_active() 528 component.get('FilterTreeView').update() 529 530 def on_menuitem_sidebar_trackers_toggled(self, widget): 531 self.config['sidebar_show_trackers'] = widget.get_active() 532 component.get('FilterTreeView').update() 533 534 def on_menuitem_sidebar_owners_toggled(self, widget): 535 self.config['sidebar_show_owners'] = widget.get_active() 536 component.get('FilterTreeView').update() 537 538 def _on_known_accounts(self, known_accounts): 539 known_accounts_to_log = [] 540 for account in known_accounts: 541 account_to_log = {} 542 for key, value in account.copy().items(): 543 if key == 'password': 544 value = '*' * len(value) 545 account_to_log[key] = value 546 known_accounts_to_log.append(account_to_log) 547 log.debug('_on_known_accounts: %s', known_accounts_to_log) 548 if len(known_accounts) <= 1: 549 return 550 551 self.builder.get_object('menuitem_change_owner').set_visible(True) 552 553 self.change_owner_submenu = Gtk.Menu() 554 self.change_owner_submenu_items = {} 555 maingroup = Gtk.RadioMenuItem() 556 557 self.change_owner_submenu_items[None] = Gtk.RadioMenuItem(maingroup) 558 559 for account in known_accounts: 560 username = account['username'] 561 item = Gtk.RadioMenuItem.new_with_label(maingroup, username) 562 self.change_owner_submenu_items[username] = item 563 self.change_owner_submenu.append(item) 564 item.connect('toggled', self._on_change_owner_toggled, username) 565 566 self.change_owner_submenu.show_all() 567 self.change_owner_submenu_items[None].set_active(True) 568 self.change_owner_submenu_items[None].hide() 569 self.builder.get_object('menuitem_change_owner').connect( 570 'activate', self._on_change_owner_submenu_active 571 ) 572 self.builder.get_object('menuitem_change_owner').set_submenu( 573 self.change_owner_submenu 574 ) 575 576 def _on_known_accounts_fail(self, reason): 577 self.builder.get_object('menuitem_change_owner').set_visible(False) 578 579 def _on_change_owner_submenu_active(self, widget): 580 log.debug('_on_change_owner_submenu_active') 581 selected = component.get('TorrentView').get_selected_torrents() 582 if len(selected) > 1: 583 self.change_owner_submenu_items[None].set_active(True) 584 return 585 586 torrent_owner = component.get('TorrentView').get_torrent_status(selected[0])[ 587 'owner' 588 ] 589 for username, item in self.change_owner_submenu_items.items(): 590 item.set_active(username == torrent_owner) 591 592 def _on_change_owner_toggled(self, widget, username): 593 log.debug('_on_change_owner_toggled') 594 update_torrents = [] 595 selected = component.get('TorrentView').get_selected_torrents() 596 for torrent_id in selected: 597 torrent_status = component.get('TorrentView').get_torrent_status(torrent_id) 598 if torrent_status['owner'] != username: 599 update_torrents.append(torrent_id) 600 601 if update_torrents: 602 log.debug('Setting torrent owner "%s" on %s', username, update_torrents) 603 604 def failed_change_owner(failure): 605 ErrorDialog( 606 _('Ownership Change Error'), 607 _('There was an error while trying changing ownership.'), 608 self.mainwindow.window, 609 details=failure.value.logable(), 610 ).run() 611 612 client.core.set_torrent_options( 613 update_torrents, {'owner': username} 614 ).addErrback(failed_change_owner) 615