summaryrefslogtreecommitdiff
path: root/trunk/dimbola/ui.py
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/dimbola/ui.py')
-rw-r--r--trunk/dimbola/ui.py385
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()
+