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