# # mainwindow.py # # Copyright (C) 2007 Andrew Resch # # Deluge is free software. # # You may redistribute it and/or modify it under the terms of the # GNU General Public License, as published by the Free Software # Foundation; either version 3 of the License, or (at your option) # any later version. # # deluge is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with deluge. If not, write to: # The Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # # In addition, as a special exception, the copyright holders give # permission to link the code of portions of this program with the OpenSSL # library. # You must obey the GNU General Public License in all respects for all of # the code used other than OpenSSL. If you modify file(s) with this # exception, you may extend this exception to your version of the file(s), # but you are not obligated to do so. If you do not wish to do so, delete # this exception statement from your version. If you delete this exception # statement from all source files in the program, then also delete it here. # # import copy import os.path import pygtk pygtk.require('2.0') import gtk import logging import urllib try: import wnck except ImportError: wnck = None from deluge.ui.client import client import deluge.component as component from deluge.configmanager import ConfigManager from deluge.ui.gtkui.ipcinterface import process_args from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning import deluge.common import common log = logging.getLogger(__name__) class _GtkBuilderSignalsHolder(object): def connect_signals(self, mapping_or_class): if isinstance(mapping_or_class, dict): for name, handler in mapping_or_class.iteritems(): if hasattr(self, name): raise RuntimeError( "A handler for signal %r has already been registered: %s" % (name, getattr(self, name)) ) setattr(self, name, handler) else: for name in dir(mapping_or_class): if not name.startswith('on_'): continue if hasattr(self, name): raise RuntimeError("A handler for signal %r has already been registered: %s" % (name, getattr(self, name))) setattr(self, name, getattr(mapping_or_class, name)) class MainWindow(component.Component): def __init__(self): if wnck: self.screen = wnck.screen_get_default() component.Component.__init__(self, "MainWindow", interval=2) self.config = ConfigManager("gtkui.conf") self.gtk_builder_signals_holder = _GtkBuilderSignalsHolder() self.main_builder = gtk.Builder() # Patch this GtkBuilder to avoid connecting signals from elsewhere # # Think about splitting up the main window gtkbuilder file into the necessary parts # in order not to have to monkey patch GtkBuilder. Those parts would then need to # be added to the main window "by hand". self.main_builder.prev_connect_signals = copy.deepcopy(self.main_builder.connect_signals) def patched_connect_signals(*a, **k): raise RuntimeError("In order to connect signals to this GtkBuilder instance please use " "'component.get(\"MainWindow\").connect_signals()'") self.main_builder.connect_signals = patched_connect_signals # Get the gtk builder file for the main window self.main_builder.add_from_file(deluge.common.resource_filename( "deluge.ui.gtkui", os.path.join("glade", "main_window.ui")) ) # The new release dialog self.main_builder.add_from_file(deluge.common.resource_filename( "deluge.ui.gtkui", os.path.join("glade", "main_window.new_release.ui")) ) # The move storage dialog self.main_builder.add_from_file(deluge.common.resource_filename( "deluge.ui.gtkui", os.path.join("glade", "main_window.move_storage.ui")) ) # The tabs self.main_builder.add_from_file(deluge.common.resource_filename( "deluge.ui.gtkui", os.path.join("glade", "main_window.tabs.ui")) ) # The tabs file menu self.main_builder.add_from_file(deluge.common.resource_filename( "deluge.ui.gtkui", os.path.join("glade", "main_window.tabs.menu_file.ui")) ) # The tabs peer menu self.main_builder.add_from_file(deluge.common.resource_filename( "deluge.ui.gtkui", os.path.join("glade", "main_window.tabs.menu_peer.ui")) ) self.window = self.main_builder.get_object("main_window") self.window.set_icon(common.get_deluge_icon()) self.vpaned = self.main_builder.get_object("vpaned") self.initial_vpaned_position = self.config["window_pane_position"] # Load the window state self.load_window_state() # Keep track of window's minimization state so that we don't update the # UI when it is minimized. self.is_minimized = False self.window.drag_dest_set(gtk.DEST_DEFAULT_ALL, [('text/uri-list', 0, 80)], gtk.gdk.ACTION_COPY) # Connect events self.window.connect("window-state-event", self.on_window_state_event) self.window.connect("configure-event", self.on_window_configure_event) self.window.connect("delete-event", self.on_window_delete_event) self.window.connect("drag-data-received", self.on_drag_data_received_event) self.vpaned.connect("notify::position", self.on_vpaned_position_event) self.window.connect("expose-event", self.on_expose_event) self.config.register_set_function("show_rate_in_title", self._on_set_show_rate_in_title, apply_now=False) client.register_event_handler("NewVersionAvailableEvent", self.on_newversionavailable_event) client.register_event_handler("TorrentFinishedEvent", self.on_torrentfinished_event) def connect_signals(self, mapping_or_class): self.gtk_builder_signals_holder.connect_signals(mapping_or_class) def first_show(self): if not(self.config["start_in_tray"] and \ self.config["enable_system_tray"]) and not \ self.window.get_property("visible"): log.debug("Showing window") self.main_builder.prev_connect_signals(self.gtk_builder_signals_holder) self.show() while gtk.events_pending(): gtk.main_iteration(False) self.vpaned.set_position(self.initial_vpaned_position) def show(self): try: component.resume("TorrentView") component.resume("StatusBar") component.resume("TorrentDetails") except: pass self.window.show() def hide(self): component.pause("TorrentView") component.get("TorrentView").save_state() component.pause("StatusBar") component.pause("TorrentDetails") # Store the x, y positions for when we restore the window self.window_x_pos = self.window.get_position()[0] self.window_y_pos = self.window.get_position()[1] self.window.hide() def present(self): # Restore the proper x,y coords for the window prior to showing it try: self.config["window_x_pos"] = self.window_x_pos self.config["window_y_pos"] = self.window_y_pos except: pass try: component.resume("TorrentView") component.resume("StatusBar") component.resume("TorrentDetails") except: pass self.window.present() self.load_window_state() def active(self): """Returns True if the window is active, False if not.""" return self.window.is_active() def visible(self): """Returns True if window is visible, False if not.""" return self.window.get_property("visible") def get_builder(self): """Returns a reference to the main window GTK builder object.""" return self.main_builder def quit(self, shutdown=False): """ Quits the GtkUI :param shutdown: whether or not to shutdown the daemon as well :type shutdown: boolean """ if shutdown: def on_daemon_shutdown(result): try: reactor.stop() except ReactorNotRunning: log.debug("Attempted to stop the reactor but it is not running...") client.daemon.shutdown().addCallback(on_daemon_shutdown) return if client.is_classicmode(): reactor.stop() return if not client.connected(): reactor.stop() return def on_client_disconnected(result): reactor.stop() client.disconnect().addCallback(on_client_disconnected) def load_window_state(self): x = self.config["window_x_pos"] y = self.config["window_y_pos"] w = self.config["window_width"] h = self.config["window_height"] self.window.move(x, y) self.window.resize(w, h) if self.config["window_maximized"]: self.window.maximize() def on_window_configure_event(self, widget, event): if not self.config["window_maximized"] and self.visible: self.config["window_x_pos"] = self.window.get_position()[0] self.config["window_y_pos"] = self.window.get_position()[1] self.config["window_width"] = event.width self.config["window_height"] = event.height def on_window_state_event(self, widget, event): if event.changed_mask & gtk.gdk.WINDOW_STATE_MAXIMIZED: if event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED: log.debug("pos: %s", self.window.get_position()) self.config["window_maximized"] = True elif not event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN: self.config["window_maximized"] = False if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED: if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED: log.debug("MainWindow is minimized..") component.pause("TorrentView") component.pause("StatusBar") self.is_minimized = True else: log.debug("MainWindow is not minimized..") try: component.resume("TorrentView") component.resume("StatusBar") except: pass self.is_minimized = False return False def on_window_delete_event(self, widget, event): if self.config["close_to_tray"] and self.config["enable_system_tray"]: self.hide() else: self.quit() return True def on_vpaned_position_event(self, obj, param): self.config["window_pane_position"] = self.vpaned.get_position() def on_drag_data_received_event(self, widget, drag_context, x, y, selection_data, info, timestamp): log.debug("Selection(s) dropped on main window %s", selection_data.data) if selection_data.get_uris(): process_args(selection_data.get_uris()) else: process_args(selection_data.data.split()) drag_context.finish(True, True) def on_expose_event(self, widget, event): component.get("SystemTray").blink(False) def stop(self): self.window.set_title("Deluge") def update(self): # Update the window title def _on_get_session_status(status): download_rate = deluge.common.fsize_short(status["payload_download_rate"]) upload_rate = deluge.common.fsize_short(status["payload_upload_rate"]) self.window.set_title("%s%s %s%s - Deluge" % (_("D:"), download_rate, _("U:"), upload_rate)) if self.config["show_rate_in_title"]: client.core.get_session_status(["payload_download_rate", "payload_upload_rate"]).addCallback(_on_get_session_status) def _on_set_show_rate_in_title(self, key, value): if value: self.update() else: self.window.set_title("Deluge") def on_newversionavailable_event(self, new_version): if self.config["show_new_releases"]: from deluge.ui.gtkui.new_release_dialog import NewReleaseDialog reactor.callLater(5.0, NewReleaseDialog().show, new_version) def on_torrentfinished_event(self, torrent_id): from deluge.ui.gtkui.notification import Notification Notification().notify(torrent_id) def is_on_active_workspace(self): # Returns True if mainwindow is on active workspace or wnck module not available if not wnck: return True for win in self.screen.get_windows(): if win.get_xid() == self.window.window.xid: return win.is_on_workspace(win.get_screen().get_active_workspace())