1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007-2009 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
10from __future__ import unicode_literals
11
12import logging
13import os.path
14from hashlib import sha1 as sha
15
16import gi
17from gi.repository import Gtk
18from gi.repository.Gdk import DragAction, WindowState
19from twisted.internet import reactor
20from twisted.internet.error import ReactorNotRunning
21
22import deluge.component as component
23from deluge.common import decode_bytes, fspeed, resource_filename
24from deluge.configmanager import ConfigManager
25from deluge.ui.client import client
26
27from .common import get_deluge_icon, windowing
28from .dialogs import PasswordDialog
29from .ipcinterface import process_args
30
31GdkX11 = None
32Wnck = None
33if windowing('X11'):
34    try:
35        from gi.repository import GdkX11
36    except ImportError:
37        pass
38
39    try:
40        gi.require_version('Wnck', '3.0')
41        from gi.repository import Wnck
42    except (ImportError, ValueError):
43        pass
44
45log = logging.getLogger(__name__)
46
47
48class _GtkBuilderSignalsHolder(object):
49    def connect_signals(self, mapping_or_class):
50
51        if isinstance(mapping_or_class, dict):
52            for name, handler in mapping_or_class.items():
53                if hasattr(self, name):
54                    raise RuntimeError(
55                        'A handler for signal %r has already been registered: %s'
56                        % (name, getattr(self, name))
57                    )
58                setattr(self, name, handler)
59        else:
60            for name in dir(mapping_or_class):
61                if not name.startswith('on_'):
62                    continue
63                if hasattr(self, name):
64                    raise RuntimeError(
65                        'A handler for signal %r has already been registered: %s'
66                        % (name, getattr(self, name))
67                    )
68                setattr(self, name, getattr(mapping_or_class, name))
69
70
71class MainWindow(component.Component):
72    def __init__(self):
73        if Wnck:
74            self.screen = Wnck.Screen.get_default()
75        component.Component.__init__(self, 'MainWindow', interval=2)
76        self.config = ConfigManager('gtk3ui.conf')
77        self.main_builder = Gtk.Builder()
78
79        # Patch this GtkBuilder to avoid connecting signals from elsewhere
80        #
81        # Think about splitting up  mainwindow gtkbuilder file into the necessary parts
82        # to avoid GtkBuilder monkey patch. Those parts would then need adding to mainwindow 'by hand'.
83        self.gtk_builder_signals_holder = _GtkBuilderSignalsHolder()
84        # FIXME: The deepcopy has been removed: copy.deepcopy(self.main_builder.connect_signals)
85        self.main_builder.prev_connect_signals = self.main_builder.connect_signals
86
87        def patched_connect_signals(*a, **k):
88            raise RuntimeError(
89                'In order to connect signals to this GtkBuilder instance please use '
90                '"component.get(\'MainWindow\').connect_signals()"'
91            )
92
93        self.main_builder.connect_signals = patched_connect_signals
94
95        # Get Gtk Builder files Main Window, New release dialog, and Tabs.
96        ui_filenames = [
97            'main_window.ui',
98            'main_window.new_release.ui',
99            'main_window.tabs.ui',
100            'main_window.tabs.menu_file.ui',
101            'main_window.tabs.menu_peer.ui',
102        ]
103        for filename in ui_filenames:
104            self.main_builder.add_from_file(
105                resource_filename(__package__, os.path.join('glade', filename))
106            )
107
108        self.window = self.main_builder.get_object('main_window')
109        self.window.set_icon(get_deluge_icon())
110        self.tabsbar_pane = self.main_builder.get_object('tabsbar_pane')
111        self.sidebar_pane = self.main_builder.get_object('sidebar_pane')
112
113        # Keep a list of components to pause and resume when changing window state.
114        self.child_components = ['TorrentView', 'StatusBar', 'TorrentDetails']
115
116        # Load the window state
117        self.load_window_state()
118
119        # Keep track of window minimization state so we don't update UI when it is minimized.
120        self.is_minimized = False
121        self.restart = False
122
123        self.window.drag_dest_set(
124            Gtk.DestDefaults.ALL,
125            [Gtk.TargetEntry.new(target='text/uri-list', flags=0, info=80)],
126            DragAction.COPY,
127        )
128
129        # Connect events
130        self.window.connect('window-state-event', self.on_window_state_event)
131        self.window.connect('configure-event', self.on_window_configure_event)
132        self.window.connect('delete-event', self.on_window_delete_event)
133        self.window.connect('drag-data-received', self.on_drag_data_received_event)
134        self.tabsbar_pane.connect(
135            'notify::position', self.on_tabsbar_pane_position_event
136        )
137        self.sidebar_pane.connect(
138            'notify::position', self.on_sidebar_pane_position_event
139        )
140        self.window.connect('draw', self.on_expose_event)
141
142        self.config.register_set_function(
143            'show_rate_in_title', self._on_set_show_rate_in_title, apply_now=False
144        )
145
146        client.register_event_handler(
147            'NewVersionAvailableEvent', self.on_newversionavailable_event
148        )
149
150    def connect_signals(self, mapping_or_class):
151        self.gtk_builder_signals_holder.connect_signals(mapping_or_class)
152
153    def first_show(self):
154        self.main_builder.prev_connect_signals(self.gtk_builder_signals_holder)
155        self.sidebar_pane.set_position(self.config['sidebar_position'])
156        self.tabsbar_pane.set_position(self.config['tabsbar_position'])
157
158        if not (
159            self.config['start_in_tray'] and self.config['enable_system_tray']
160        ) and not self.window.get_property('visible'):
161            log.debug('Showing window')
162            self.show()
163
164        while Gtk.events_pending():
165            Gtk.main_iteration()
166
167    def show(self):
168        component.resume(self.child_components)
169        self.window.show()
170
171    def hide(self):
172        component.get('TorrentView').save_state()
173        component.pause(self.child_components)
174        self.save_position()
175        self.window.hide()
176
177    def present(self):
178        def restore():
179            # Restore the proper x,y coords for the window prior to showing it
180            component.resume(self.child_components)
181            timestamp = self.get_timestamp()
182            if windowing('X11'):
183                # Use present with X11 set_user_time since
184                # present_with_time is inconsistent.
185                self.window.present()
186                self.window.get_window().set_user_time(timestamp)
187            else:
188                self.window.present_with_time(timestamp)
189            self.load_window_state()
190
191        if self.config['lock_tray'] and not self.visible():
192            dialog = PasswordDialog(_('Enter your password to show Deluge...'))
193
194            def on_dialog_response(response_id):
195                if response_id == Gtk.ResponseType.OK:
196                    if (
197                        self.config['tray_password']
198                        == sha(decode_bytes(dialog.get_password()).encode()).hexdigest()
199                    ):
200                        restore()
201
202            dialog.run().addCallback(on_dialog_response)
203        else:
204            restore()
205
206    def get_timestamp(self):
207        """Returns the timestamp for the windowing server."""
208        timestamp = 0
209        gdk_window = self.window.get_window()
210        if GdkX11 and isinstance(gdk_window, GdkX11.X11Window):
211            timestamp = GdkX11.x11_get_server_time(gdk_window)
212        return timestamp
213
214    def active(self):
215        """Returns True if the window is active, False if not."""
216        return self.window.is_active()
217
218    def visible(self):
219        """Returns True if window is visible, False if not."""
220        return self.window.get_visible()
221
222    def get_builder(self):
223        """Returns a reference to the main window GTK builder object."""
224        return self.main_builder
225
226    def quit(self, shutdown=False, restart=False):  # noqa: A003 python builtin
227        """Quits the GtkUI application.
228
229        Args:
230            shutdown (bool): Whether or not to shutdown the daemon as well.
231            restart (bool): Whether or not to restart the application after closing.
232
233        """
234
235        def quit_gtkui():
236            def stop_gtk_reactor(result=None):
237                self.restart = restart
238                try:
239                    reactor.callLater(0, reactor.fireSystemEvent, 'gtkui_close')
240                except ReactorNotRunning:
241                    log.debug('Attempted to stop the reactor but it is not running...')
242
243            if shutdown:
244                client.daemon.shutdown().addCallback(stop_gtk_reactor)
245            elif not client.is_standalone() and client.connected():
246                client.disconnect().addCallback(stop_gtk_reactor)
247            else:
248                stop_gtk_reactor()
249
250        if self.config['lock_tray'] and not self.visible():
251            dialog = PasswordDialog(_('Enter your password to Quit Deluge...'))
252
253            def on_dialog_response(response_id):
254                if response_id == Gtk.ResponseType.OK:
255                    if (
256                        self.config['tray_password']
257                        == sha(decode_bytes(dialog.get_password()).encode()).hexdigest()
258                    ):
259                        quit_gtkui()
260
261            dialog.run().addCallback(on_dialog_response)
262        else:
263            quit_gtkui()
264
265    def load_window_state(self):
266        if (
267            self.config['window_x_pos'] == -32000
268            or self.config['window_x_pos'] == -32000
269        ):
270            self.config['window_x_pos'] = self.config['window_y_pos'] = 0
271
272        self.window.move(self.config['window_x_pos'], self.config['window_y_pos'])
273        self.window.resize(self.config['window_width'], self.config['window_height'])
274        if self.config['window_maximized']:
275            self.window.maximize()
276
277    def save_position(self):
278        self.config['window_maximized'] = self.window.props.is_maximized
279        if not self.config['window_maximized'] and self.visible():
280            self.config['window_x_pos'], self.config[
281                'window_y_pos'
282            ] = self.window.get_position()
283            self.config['window_width'], self.config[
284                'window_height'
285            ] = self.window.get_size()
286
287    def on_window_configure_event(self, widget, event):
288        self.save_position()
289
290    def on_window_state_event(self, widget, event):
291        if event.changed_mask & WindowState.ICONIFIED:
292            if event.new_window_state & WindowState.ICONIFIED:
293                log.debug('MainWindow is minimized..')
294                component.get('TorrentView').save_state()
295                component.pause(self.child_components)
296                self.is_minimized = True
297            else:
298                log.debug('MainWindow is not minimized..')
299                component.resume(self.child_components)
300                self.is_minimized = False
301        return False
302
303    def on_window_delete_event(self, widget, event):
304        if self.config['close_to_tray'] and self.config['enable_system_tray']:
305            self.hide()
306        else:
307            self.quit()
308
309        return True
310
311    def on_tabsbar_pane_position_event(self, obj, param):
312        self.config['tabsbar_position'] = self.tabsbar_pane.get_position()
313
314    def on_sidebar_pane_position_event(self, obj, param):
315        self.config['sidebar_position'] = self.sidebar_pane.get_position()
316
317    def on_drag_data_received_event(
318        self, widget, drag_context, x, y, selection_data, info, timestamp
319    ):
320        log.debug('Selection(s) dropped on main window %s', selection_data.get_text())
321        if selection_data.get_uris():
322            process_args(selection_data.get_uris())
323        else:
324            process_args(selection_data.get_text().split())
325        drag_context.finish(True, True, timestamp)
326
327    def on_expose_event(self, widget, event):
328        component.get('SystemTray').blink(False)
329
330    def stop(self):
331        self.window.set_title('Deluge')
332
333    def update(self):
334        # Update the window title
335        def _on_get_session_status(status):
336            download_rate = fspeed(
337                status['payload_download_rate'], precision=0, shortform=True
338            )
339            upload_rate = fspeed(
340                status['payload_upload_rate'], precision=0, shortform=True
341            )
342            self.window.set_title(
343                _('D: {download_rate} U: {upload_rate} - Deluge').format(
344                    download_rate=download_rate, upload_rate=upload_rate
345                )
346            )
347
348        if self.config['show_rate_in_title']:
349            client.core.get_session_status(
350                ['payload_download_rate', 'payload_upload_rate']
351            ).addCallback(_on_get_session_status)
352
353    def _on_set_show_rate_in_title(self, key, value):
354        if value:
355            self.update()
356        else:
357            self.window.set_title(_('Deluge'))
358
359    def on_newversionavailable_event(self, new_version):
360        if self.config['show_new_releases']:
361            from .new_release_dialog import NewReleaseDialog
362
363            reactor.callLater(5.0, NewReleaseDialog().show, new_version)
364
365    def is_on_active_workspace(self):
366        """Determines if MainWindow is on the active workspace.
367
368        Returns:
369            bool: True if on active workspace (or wnck module not available), otherwise False.
370
371        """
372
373        if Wnck:
374            self.screen.force_update()
375            win = Wnck.Window.get(self.window.get_window().get_xid())
376            if win:
377                active_wksp = win.get_screen().get_active_workspace()
378                if active_wksp:
379                    return win.is_on_workspace(active_wksp)
380                return False
381        return True
382