# Copyright (C) 2009 Lars Wirzenius # # This program is free software: you can 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. # # This program 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 this program. If not, see . # This is necessary for running under Python 2.5, which we need to # do on Debian, for now. from __future__ import with_statement import datetime import logging import optparse import os import Queue import shutil import subprocess import sys import tempfile import time import glib import gobject import gtk import dimbola import gtkapp import pluginmgr GLADE = os.path.join(os.path.dirname(__file__), 'ui.ui') MIN_WEIGHT = 0 MAX_WEIGHT = 2**31 class BackgroundStatus(object): '''Status change indications from background jobs to UI. action should be either 'start' or 'stop'. description is a user-visible description of what action is going in. ''' def __init__(self, action, description): self.action = action self.description = description def process_result(self, mwc): '''Do something with the result, in the MWC context. This will only ever be called by MWC when action is 'stop'. ''' class MainWindowController(gobject.GObject, gtkapp.GtkApplication): __gsignals__ = { 'setup-widgets': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []), 'db-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []), 'photo-meta-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_INT]), } def __init__(self, pm): gobject.GObject.__init__(self) # Set up the plugin manager. Every Dimbola plugin gets this # class as their initializer argument. self.pm = pm self.pm.plugin_arguments = (self,) # The currently open database. See the get_db/set_db methods # later on. self._db = None self.grid = dimbola.Grid(self) self.preferences = dimbola.Preferences() # Load all .ui files and setup all widgets. for ui_file in self.find_ui_files(): self.setup_widgets(ui_file, [self, self.grid, self.preferences] + self.pm.plugins) self.weigh_predefined_menu_items() self.preferences.init(self) self.emit('setup-widgets') self.grid.model.connect('selection-changed', self.grid_selection_changed) self.set_default_size() self.bgmgr = dimbola.BackgroundManager() self.bg_idle_id = None self.bg_total = 0 self.bg_done = 0 self.enable_plugins() def enable_plugins(self): '''Enable all plugins that are intended to be enabled.''' for plugin in self.pm.plugins: if self.preferences.plugin_is_enabled(plugin): plugin.enable() def find_ui_files(self): '''Find all .ui files: the main one, plus those for plugins.''' ui_files = [GLADE] for plugin in self.pm.plugins: module = sys.modules[plugin.__module__] pathname, ext = os.path.splitext(module.__file__) assert pathname.endswith('_plugin') pathname = pathname[:-len('_plugin')] + '.ui' if os.path.exists(pathname): ui_files.append(pathname) return ui_files def photo_meta_changed(self, photoid): self.emit('photo-meta-changed', photoid) def new_hook(self, name, return_type, param_types): gobject.signal_new(name, self.__class__, gobject.SIGNAL_RUN_LAST, return_type, param_types) def get_db(self): return self._db def set_db(self, db): self._db = db self.emit('db-changed') db = property(get_db, set_db) def set_default_size(self): self.widgets['window'].set_default_size(900, 700) self.widgets['window'].maximize() self.widgets['left_sidebar'].set_size_request(250, -1) self.widgets['right_sidebar'].set_size_request(250, -1) def run(self): self.widgets['window'].show() self.set_sensitive() gtk.main() def handle_background_status(self): if self.bgmgr.running: try: result = self.bgmgr.results.get(block=False) except Queue.Empty: pass else: if isinstance(result, dimbola.BackgroundStatus): text = result.description if result.action == 'stop': result.process_result(self) self.bg_done += 1 elif result is None: text = '' self.bg_done += 1 else: self.error_message('Oops', str(result)) text = 'Error' self.bg_done += 1 f = float(self.bg_done) / float(self.bg_total) p = self.widgets['bg_progressbar'] p.set_fraction(f) p.set_text('%d / %d' % (self.bg_done, self.bg_total)) self.widgets['bg_label'].set_text(text) self.set_sensitive() return True else: self.bg_idle_id = None self.widgets['bg_label'].set_text('') p = self.widgets['bg_progressbar'] p.set_fraction(0.0) p.set_text('') self.bg_done = 0 self.bg_total = 0 self.set_sensitive() return False def add_bgjob(self, job): self.bgmgr.add_job(job) self.bg_total += 1 if not self.bgmgr.processes: self.bgmgr.start_jobs() if self.bg_idle_id is None: self.bg_idle_id = glib.idle_add(self.handle_background_status) p = self.widgets['bg_progressbar'] p.set_fraction(0.0) p.set_text('%d / %d' % (self.bg_done, self.bg_total)) self.set_sensitive() def bg_stop_button_is_sensitive(self): return self.bgmgr.running def error_message(self, msg1, msg2): dialog = self.widgets['error_dialog'] dialog.set_markup(msg1) dialog.format_secondary_text(msg2) dialog.show() dialog.run() dialog.hide() def add_to_menu(self, menu_name, menuitem_name, label, check=False, weight=MAX_WEIGHT): '''Add an item to a menu. menu_name is the name of the menu in the .ui file. menuitem_name is the name of the new menu item. label is the text of the new menu item. check is True if the new item is to be a check item. weight gives the ordering inside the menu, relative to other items. ''' assert weight >= MIN_WEIGHT assert weight <= MAX_WEIGHT menu = self.widgets[menu_name] if menuitem_name in [i.get_name() for i in menu.get_children()]: raise Exception('Attempting to re-add menu item %s to %s' % (menuitem_name, menu_name)) if check: menuitem = gtk.CheckMenuItem(label) else: menuitem = gtk.MenuItem(label) menuitem.set_name(menuitem_name) menuitem.show() menuitem.set_data('weight', weight) menu.append(menuitem) self.reorder_within_parent(menu, menuitem) self.setup_a_widget(menuitem) def remove_from_menu(self, menu_name, menuitem_name): menu = self.widgets[menu_name] for menuitem in menu.get_children(): if menuitem.get_name() == menuitem_name: menu.remove(menuitem) def add_to_sidebar(self, sidebar_name, widget_name, expand=False, fill=False, weight=MAX_WEIGHT): '''Add an item to a sidebar. sidebar_name is the name of the sidebar in the .ui file. widget_name is the name of the widget, also from (some) .ui file. expand and fill are given to the gtk.Box.pack_start method. weight gives the ordering inside the menu, relative to other items. ''' assert weight >= MIN_WEIGHT assert weight <= MAX_WEIGHT sidebar = self.widgets[sidebar_name] widget = self.widgets[widget_name] widget.set_data('weight', weight) sidebar.pack_start(widget, expand=expand, fill=fill, padding=6) self.reorder_within_parent(sidebar, widget) def remove_from_sidebar(self, sidebar_name, widget_name): sidebar = self.widgets[sidebar_name] widget = self.widgets[widget_name] sidebar.remove(widget) def reorder_within_parent(self, parent, widget): weight = widget.get_data('weight') for i, child in enumerate(parent.get_children()): if weight < child.get_data('weight'): parent.reorder_child(widget, i) break def weigh_predefined_menu_items(self): '''Set weights for all menu items for main_menu. This is used so that the default items and items added by plugins get ordered in a nice, deterministic manner. Each menu may contain an invisible menu item named with a 'plugin_items' prefix. Any menu items before such an item are given a MIN_WEIGHT-1 weight; anything after (and the invisible one itself), a MAX_WEIGHT+1 weight. If a menu does not have a 'plugin_items' item, all weights will be MIN_WEIGHT-1. ''' for menu in self.find_menus(): children = menu.get_children() for pos, child in enumerate(children): if child.get_name().startswith('plugin_items'): break for child in children[:pos]: child.set_data('weight', MIN_WEIGHT - 1) for child in children[pos:]: child.set_data('weight', MAX_WEIGHT + 1) def find_menus(self): '''Generator for finding all menus under the main menu.''' for item in self.widgets.values(): if isinstance(item, gtk.Menu): yield item def load_thumbnails_from_database(self): with self.db: for photoid in self.grid.model.photoids: a, b, c, rotate = self.db.get_basic_photo_metadata(photoid) thumbnail = self.db.get_thumbnail(photoid) pixbuf = dimbola.image_data_to_pixbuf(thumbnail) self.grid.model.thumbnails[photoid] = pixbuf self.grid.model.angles[photoid] = rotate self.grid.view.draw_thumbnail(photoid) # The rest are GTK signal callbacks. def on_window_delete_event(self, *args): self.bgmgr.stop_jobs() gtk.main_quit() on_quit_menuitem_activate = on_window_delete_event def on_about_menuitem_activate(self, *args): w = self.widgets['aboutdialog'] w.set_version(dimbola.version) w.show() w.run() w.hide() def grid_selection_changed(self, *args): self.set_sensitive() def on_bg_stop_button_clicked(self, *args): self.bgmgr.stop_jobs() self.set_sensitive() class UI(object): '''Graphical user interface.''' def create_option_parser(self): # pragma: no cover """Create an OptionParser instance for this app.""" parser = optparse.OptionParser(version=dimbola.version) return parser def parse_command_line(self): # pragma: no cover """Parse the command line for this app.""" parser = self.create_option_parser() options, args = parser.parse_args() return options, args def run(self): # pragma: no cover logging.basicConfig(level=logging.INFO) options, args = self.parse_command_line() if not args: db_name = 'default.dimbola' else: db_name = args[0] db = dimbola.Database(db_name) db.init_db() pm = pluginmgr.PluginManager() pm.locations = [os.path.join(os.path.dirname(dimbola.__file__), 'plugins')] mwc = MainWindowController(pm) mwc.db = db mwc.run()