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