diff options
Diffstat (limited to 'trunk/dimbola/ui.py')
-rw-r--r-- | trunk/dimbola/ui.py | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/trunk/dimbola/ui.py b/trunk/dimbola/ui.py new file mode 100644 index 0000000..6e655c5 --- /dev/null +++ b/trunk/dimbola/ui.py @@ -0,0 +1,385 @@ +# Copyright (C) 2009 Lars Wirzenius <liw@liw.fi> +# +# 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 <http://www.gnu.org/licenses/>. + + +# 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() + |