added a menubar
[marvin.git] / marvin / view.py
blob4db37337ca6c5e16adb5af5cc1547790335fea88
1 ##
2 ## view.py
3 ## Login : <freyes@yoda>
4 ## Started on Tue Jul 1 20:51:33 2008 Felipe Reyes
5 ## $Id$
6 ##
7 ## Copyright (C) 2008 Felipe Reyes
8 ##
9 ## This file is part of Marvin.
11 ## Marvin is free software: you can redistribute it and/or modify
12 ## it under the terms of the GNU General Public License as published by
13 ## the Free Software Foundation, either version 3 of the License, or
14 ## (at your option) any later version.
16 ## Marvin is distributed in the hope that it will be useful,
17 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ## GNU General Public License for more details.
21 ## You should have received a copy of the GNU General Public License
22 ## along with this program. If not, see <http://www.gnu.org/licenses/>.
24 from marvin.model import Photo, PhotoListStore, connect_to_db, ConfigurationManager
26 import locale
27 import gettext
28 import gobject
29 import gtk
30 import gtk.glade
31 import gtk.gdk
32 import gnomevfs
33 import gconf
34 import threading
35 import sqlite3
36 import os
37 import sys
38 import shutil
39 import time
40 import datetime
41 try:
42 import EXIF
43 except:
44 curr_dir = os.path.abspath(".")
45 sys.path.append(curr_dir)
46 import EXIF
48 ##logging system
49 import logging
50 log = None
52 ### l18n
53 APP_NAME = "marvin"
54 #Get the local directory since we are not installing anything
55 local_path = os.path.realpath(os.path.dirname(sys.argv[0]))
56 # Init the list of languages to support
57 langs = []
58 #Check the default locale
59 lc, encoding = locale.getdefaultlocale()
60 if (lc):
61 #If we have a default, it's the first in the list
62 langs = [lc]
63 # Now lets get all of the supported languages on the system
64 language = os.environ.get('LANGUAGE', None)
65 if (language):
66 langs += language.split(":")
68 langs += ["es_CL"]
70 gettext.bindtextdomain(APP_NAME, local_path)
71 gettext.textdomain(APP_NAME)
72 # Get the language to use
73 lang = gettext.translation(APP_NAME, local_path, languages=langs, fallback = True)
75 _ = lang.gettext
76 ###
78 gtk.gdk.threads_init()
79 wTree = None
81 from pkg_resources import resource_filename
84 class MainWindow (object):
86 def __init__ (self, **kwargs):
88 self._gladefile = resource_filename("marvin.ui", "marvin.glade")
89 global wTree
90 wTree = gtk.glade.XML(self._gladefile, "wnd_main")
92 signals = {
93 "on_entry_sql_filter_activate" : self._on_sql_filter_activate,
94 "on_btn_search_with_filter_clicked" : self._on_search_clicked,
95 "on_iconview1_selection_changed" : self._on_selection_changed,
96 "on_wnd_main_delete_event" : self._on_delete_event,
97 "on_iconview1_item_activated" : self._item_activated,
98 "on_iconview1_button_press_event" : self._show_popup,
101 wTree.signal_autoconnect(signals)
103 list_photos = gtk.ListStore(Photo, gtk.gdk.Pixbuf, str)
104 list_photos.clear()
106 #setup the iconview and set the model
107 iconview = wTree.get_widget("iconview1")
108 iconview.set_model(list_photos)
109 iconview.set_pixbuf_column(1)
110 iconview.set_text_column(2)
112 self._win = wTree.get_widget("wnd_main")
113 uidef = resource_filename("marvin.ui", "bar.xml")
114 self._uimanager = gtk.UIManager()
115 accel_group = self._uimanager.get_accel_group()
116 self._win.add_accel_group(accel_group)
117 action_group = gtk.ActionGroup('my_actions')
118 action_group.add_actions([#TODO:finish actions
119 ('File', None, _("_File"), None, None, None),
120 ('ImportPhoto', gtk.STOCK_ADD, _("Import Photos"), "<Control>i", None,
121 self._on_import_clicked),
122 ("Quit", gtk.STOCK_QUIT, None, "<Control>q", None,
123 self._on_quit_clicked),
124 ("Image", None, _("_Image"), None, None, None),
125 ('SetAsWallpaper', None, _("Set as wallpaper"), None, None,
126 self._on_set_wallpaper_clicked),
129 self._uimanager.insert_action_group(action_group, 0)
130 self._uimanager.add_ui_from_file(uidef)
131 box = wTree.get_widget("vbox1")
132 menubar = self._uimanager.get_widget("/menubar")
133 box.pack_start(menubar, False)
134 box.reorder_child(menubar, 0)
135 toolbar = self._uimanager.get_widget("/toolbar")
136 box.pack_start(toolbar, False)
137 box.reorder_child(toolbar, 1)
139 config = ConfigurationManager()
140 self._win.set_default_size(config.window_default_size[0],
141 config.window_default_size[1])
142 self._win.show_all()
145 # prepare and launch the thread that fills the iconview model
146 self._thread = PhotoListThread(list_photos)
147 self._thread.start()
149 def _show_popup(self, widget, event):
150 if event.button != 3:
151 return
153 iconview = wTree.get_widget("iconview1")
154 selected_items = iconview.get_selected_items()
155 item_under_mouse = iconview.get_item_at_pos (int(event.x), int(event.y))[0]
157 if item_under_mouse not in selected_items:
158 iconview.unselect_all()
159 iconview.select_path(item_under_mouse)
160 selected_items = iconview.get_selected_items()
162 item = self._uimanager.get_widget("/popup/SetAsWallpaper")
163 if len(selected_items) != 1:
164 item.set_sensitive(False)
165 else:
166 item.set_sensitive(True)
168 menu_widget = self._uimanager.get_widget("/popup");
170 menu_widget.popup(None, None, None, event.button, event.time);
171 menu_widget.show_all()
173 def _on_quit_clicked (self, menuitem):
174 self.quit()
176 def _on_set_wallpaper_clicked(self, menuitem):
178 iconview = wTree.get_widget("iconview1")
180 selected_items = iconview.get_selected_items()
182 if len(selected_items) != 1:
183 return
185 model = iconview.get_model()
186 iter = model.get_iter(selected_items[0])
187 photo = model.get(iter, 0)[0]
189 ConfigurationManager.set_wallpaper(photo.path)
191 def _on_import_clicked(self, menuitem):
192 ImportWindow()
194 def _on_sql_filter_activate(self, entry):
195 button = wTree.get_widget("btn_search_with_filter")
196 button.clicked()
198 def _on_search_clicked(self, button):
201 Arguments:
202 - `self`:
203 - `button`:
205 model = wTree.get_widget("iconview1").get_model()
207 entry = wTree.get_widget("entry_sql_filter")
208 sql_filter = entry.get_text()
210 query = "select id, uri from photos " + sql_filter
212 if not self._thread.stop.isSet():
213 self._thread.stop.set()
214 thread = PhotoListThread(model, query)
215 thread.start()
217 def _on_selection_changed(self, iconview):
220 Arguments:
221 - `self`:
222 - `iconview`:
224 num_selec_items = len(iconview.get_selected_items())
225 if num_selec_items == 1:
227 lbl = wTree.get_widget("lbl_original_date")
229 model = iconview.get_model()
230 iter = model.get_iter(iconview.get_selected_items()[0])
231 photo = model.get(iter, 0)[0]
232 str_date = photo.original_date.strftime("%x")
233 lbl.set_text(_("Original Date %s" % str_date))
235 lbl = wTree.get_widget("lbl_tags")
237 lbl.set_text(str(photo.tags))
238 elif num_selec_items == 0:
239 wTree.get_widget("lbl_original_date").set_text("")
240 wTree.get_widget("lbl_tags").set_text("")
242 def _item_activated(self, iconview, path):
244 store = iconview.get_model()
245 iter = store.get_iter(path)
246 photo = store.get_value (iter, 0)
248 ImageWindow(photo, iconview)
250 def _on_delete_event (self, widget, event):
251 self.quit()
253 def quit(self):
254 config = ConfigurationManager()
255 win = wTree.get_widget("wnd_main")
256 config.window_default_size = win.get_size()
257 gtk.main_quit()
259 class ImageWindow:
261 def __init__(self, photo, iconview):
263 self.fullscreen = False
265 self._wTree = gtk.glade.XML(resource_filename("marvin.ui", "marvin.glade"), "wnd_display_img")
267 signals = {
268 "on_wnd_display_img_window_state_event": self._on_window_state_event,
269 "on_wnd_display_img_delete_event" : self._on_delete_event,
270 "on_wnd_display_img_key_press_event" : self._on_key_press,
272 self._wTree.signal_autoconnect (signals)
274 self.img_widget = self._wTree.get_widget("img")
276 self.iconview = iconview
277 self.photo = photo
278 self.path = gnomevfs.get_local_path_from_uri (self.photo.uri)
280 win = self._wTree.get_widget("wnd_display_img")
281 win.set_size_request (800, 600)
282 win.show_all()
284 self.update_image()
286 def _on_delete_event(self, widget, event):
289 Arguments:
290 - `self`:
291 - `widget`:
292 - `event`:
294 pass
296 def _on_window_state_event (self, widget, event):
297 "This method should be used for the fullscreen functinality"
298 pass
301 def _on_key_press(self, widget, event):
302 """Handle the key press event of the image window
304 Arguments:
305 - `self`:
306 - `widget`:
307 - `event`:
309 model = self.iconview.get_model()
311 iter = model.get_iter(self.iconview.get_selected_items()[0])
313 str_key = gtk.gdk.keyval_name(event.keyval)
315 if str_key == 'Right' or str_key == 'Down':
316 iter_next = model.iter_next(iter)
318 if iter_next == None:
319 iter_next = model.get_iter_first()
321 self.update_iconview(iter_next)
323 elif str_key == 'Left' or str_key == 'Up':
324 int_path = int(model.get_string_from_iter(iter)) - 1
325 if int_path < 0:
326 int_path = 0
327 str_path = str("%d" % (int_path))
329 iter_next = model.get_iter_from_string(str_path)
331 self.update_iconview(iter_next)
333 elif str_key == 'plus':
334 self.zoom(1.1)
336 elif str_key == 'minus':
337 self.zoom(0.9)
339 elif str_key == '1':
340 self.update_image(False)
342 elif str_key == 'F11':
343 win = self._wTree.get_widget("wnd_display_img")
344 if self.fullscreen:
345 win.unfullscreen()
346 self.fullscreen =False
347 else:
348 win.fullscreen()
349 self.fullscreen = True
351 elif str_key.lower() == 's':
352 self.update_image()
354 elif str_key == 'Escape':
355 win = self._wTree.get_widget("wnd_display_img")
356 win.destroy()
357 return
358 else:
359 print str_key
360 return
363 def zoom(self, factor):
365 curr_width = self.pixbuf.get_width()
366 curr_height = self.pixbuf.get_height()
367 scaled_pixbuf = self.pixbuf.scale_simple(int(curr_width*factor), int(curr_height*factor), gtk.gdk.INTERP_BILINEAR)
369 self.pixbuf = scaled_pixbuf
370 self.img_widget.set_from_pixbuf(self.pixbuf)
373 def update_iconview(self, iter_next):
375 self.photo = model.get(iter_next, 0)[0]
376 self.path = gnomevfs.get_local_path_from_uri (self.photo.uri)
378 self.update_image()
379 str_path = model.get_string_from_iter(iter_next)
380 self.iconview.select_path(str_path)
382 def update_image(self, to_wnd_size=True):
384 if (to_wnd_size):
385 win = self._wTree.get_widget("wnd_display_img")
386 size = win.get_size()
387 self.pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(self.path, size[0], size[1])
388 else:
389 self.pixbuf = gtk.gdk.pixbuf_new_from_file(self.path)
391 self.img_widget.set_from_pixbuf(self.pixbuf)
393 class PhotoListThread (threading.Thread):
395 def __init__ (self, model, query=None, use_progressbar=False):
396 super(PhotoListThread, self).__init__()
398 if query is not None:
399 self._query = query
400 else:
401 self._query = "select id, uri from photos order by time desc limit 10"
403 self._model = model
405 def run (self):
407 inicio = datetime.datetime.now()
409 global wTree
411 gtk.gdk.threads_enter()
412 progressbar = wTree.get_widget("progressbar1")
413 progressbar.show()
414 self._model.clear()
415 gtk.gdk.threads_leave()
417 conn = connect_to_db()
418 c = conn.cursor()
419 c.execute (self._query)
421 for row in c:
423 gtk.gdk.threads_enter()
424 i += 1
425 try:
426 p = Photo(row[0], row[1])
427 self._model.append([p,
428 p.pixbuf,
429 p.original_date.date().strftime("%x")])
430 except:
431 pass
433 progressbar.pulse()
434 gtk.gdk.threads_leave()
436 gtk.gdk.threads_enter()
437 Progressbar.push_message(_("%d pictures found.") % (i))
438 conn.close()
439 progressbar.hide()
440 gtk.gdk.threads_leave()
442 fin = datetime.datetime.now()
443 print "Took %s load the pictures" % (fin-inicio)
445 class Progressbar(object):
447 @classmethod
448 def push_message(cls, msg, context="marvin"):
449 statusbar = wTree.get_widget("statusbar1")
450 context_id = statusbar.get_context_id(context)
451 msg_id = statusbar.push(context_id, msg)
452 gobject.timeout_add(10000,Progressbar.remove_from_statusbar,context_id, msg_id)
454 @classmethod
455 def remove_from_statusbar(cls, context_id, msg_id):
456 statusbar = wTree.get_widget("statusbar1")
457 statusbar.remove(context_id, msg_id)
459 return False #stop being called by gobject.timeout_add
462 class ImportWindow(object):
466 def __init__(self):
469 self._thread = None
470 glade_file = resource_filename("marvin.ui", "marvin.glade")
471 self._wTree = gtk.glade.XML(glade_file, "wnd_photo_import")
473 signals = {
474 "on_checkbtn_include_subdirs_toggled" : self._on_include_subdir_toggled,
475 "on_chooserbtn_dir_to_import_current_folder_changed" : self._on_folder_changed,
476 "on_btn_accept_clicked" : self._on_accept_clicked,
479 self._wTree.signal_autoconnect(signals)
481 list_photos = gtk.ListStore(Photo, gtk.gdk.Pixbuf, str)
482 list_photos.clear()
483 iconview = self._wTree.get_widget("iconview_preview")
484 iconview.set_model(list_photos)
485 iconview.set_pixbuf_column(1)
486 iconview.set_text_column(2)
488 win = self._wTree.get_widget("wnd_photo_import")
489 win.show_all()
491 def _on_folder_changed(self, widget):
492 self.refresh_iconview()
494 def _on_include_subdir_toggled(self, togglebutton):
495 self.refresh_iconview()
497 def refresh_iconview(self):
499 uris_list = list()
501 uri = self._wTree.get_widget("chooserbtn_dir_to_import").get_current_folder_uri()
502 selected_path= gnomevfs.get_local_path_from_uri(uri)
503 model = self._wTree.get_widget("iconview_preview").get_model()
505 recursive = self._wTree.get_widget("checkbtn_include_subdirs").get_active()
507 if recursive:
508 for root, dirs, files in os.walk(selected_path):
509 for name in files:
510 uri = "file://" + str(root) + "/" + str(name)
511 if gnomevfs.get_mime_type(uri) == "image/jpeg":
512 uris_list.append(uri)
513 else:
514 for name in os.listdir(selected_path):
515 uri = "file://" + str(selected_path) + "/" + str(name)
516 if gnomevfs.get_mime_type(uri) == "image/jpeg":
517 uris_list.append(uri)
519 if (self._thread is not None) and (not self._thread.is_stopped()):
520 self._thread.stop()
522 self._thread = PhotoPreviewThread(model, uris_list)
523 self._thread.start()
525 def _on_accept_clicked(self, button):
527 self._wTree.get_widget("wnd_photo_import").set_sensitive(False)
529 iconview = self._wTree.get_widget("iconview_preview")
530 model = iconview.get_model()
532 list_of_photos = list()
534 if len(iconview.get_selected_items()) == 0:
535 iter = model.get_iter_first()
537 while (iter is not None):
538 list_of_photos.append(model.get(iter, 0)[0])
539 iter = model.iter_next(iter)
541 else:
542 selected_items = iconview.get_selected_items()
543 for path in selected_items:
544 iter = model.get_iter(path)
545 list_of_photos.append(model.get(iter, 0)[0])
547 copy_to_photodir = self._wTree.get_widget("checkbtn_copy").get_active()
548 progressbar = self._wTree.get_widget("progressbar_import")
549 thread = ImportPhotoThread(list_of_photos, copy_to_photodir, progressbar)
550 thread.start()
552 class ImportPhotoThread(threading.Thread):
554 def __init__(self, list_of_photos, copy_to_photodir, progressbar):
555 super(ImportPhotoThread, self).__init__()
557 self._photos = list_of_photos
558 self._copy_to_photodir = copy_to_photodir
559 self._progressbar = progressbar
561 def run(self):
562 self._conn = connect_to_db()
563 self._c = self._conn.cursor()
565 self._progressbar.show()
566 i = 0
567 total = float(len(self._photos))
568 for photo in self._photos:
569 i += 1
570 self._import (photo, self._copy_to_photodir)
571 self._progressbar.set_fraction(i/total)
572 self._progressbar.set_text(_("Imported %d of %d") % (i, int(total)))
574 self._conn.commit()
575 self._conn.close()
577 win = self._progressbar.get_toplevel()
578 win.destroy()
580 def _import (self, photo, copy_to_photodir):
582 if copy_to_photodir:
583 new_uri = self._copy_photo(photo.uri)
584 else:
585 new_uri = photo.uri
587 self._c.execute("""insert into photos(uri, time, description, roll_id, default_version_id, rating)
588 values (?, ?, '', 0, 1, 0)""", (new_uri, time.mktime(photo.original_date.timetuple()),))
590 def _copy_photo(self, uri):
592 base = "/apps/f-spot/import"
593 client = gconf.client_get_default()
594 client.add_dir(base, gconf.CLIENT_PRELOAD_NONE)
595 root_photo_dir = client.get_string(base + "/storage_path")
597 src_path = gnomevfs.get_local_path_from_uri(uri)
598 f = open(src_path, 'rb')
599 tags = EXIF.process_file(f, stop_tag='DateTimeOriginal')
601 if tags.has_key('EXIF DateTimeOriginal'):
602 (aux_date, aux_time) = str(tags['EXIF DateTimeOriginal']).split(' ')
603 (year, month, day) = aux_date.split(':')
605 else:
606 mtime = time.localtime(os.path.getmtime(src_path))
607 year = str(mtime[0])
608 moth = str(mtime[1])
609 day = str(mtime[2])
612 destdir = os.path.join(root_photo_dir, year, month, day)
613 list_dir = (os.path.join(root_photo_dir, year),
614 os.path.join(root_photo_dir, year, month),
615 os.path.join(root_photo_dir, year, month, day))
616 for aux_path in list_dir:
617 if not os.path.exists(aux_path):
618 os.mkdir(aux_path)
620 if os.path.exists(os.path.join(destdir, os.path.basename(src_path))):
621 aux = os.path.basename(src_path).rsplit('.', 1)
622 suff = str(time.localtime()[3]) + str(time.localtime()[4])
623 destdir = os.path.join(destdir, aux[0] + suff + aux[1])
624 else:
625 destdir = os.path.join(destdir, os.path.basename(src_path))
627 shutil.copyfile(src_path, destdir)
629 return "file://" + destdir #returns the destination uri
631 class PhotoPreviewThread(threading.Thread):
633 def __init__(self, model, uris):
634 super(PhotoPreviewThread, self).__init__()
636 self._model = model
637 self._uris = uris
638 self._stop = False
639 self._stopped = True
641 def run(self):
642 self._stopped = False
643 self._model.clear()
645 for uri in self._uris:
646 if self._stop:
647 self._stopped = True
648 self._stop = False
649 return
651 photo = Photo(-1, uri, 150)
652 self._model.append([photo,
653 photo.pixbuf,
654 photo.original_date.date().strftime("%x")])
656 def stop(self):
657 self._stop = True
659 def is_stopped(self):
660 return self._stopped
662 def setup_log():
663 # code adapted from
664 # http://docs.python.org/lib/multiple-destinations.html example
665 # set up logging to file - see previous section for more details
666 logging.basicConfig(level=logging.DEBUG,
667 #format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
668 format='%(levelname)-8s %(message)s',
669 datefmt='%m-%d %H:%M')
670 # define a Handler which writes INFO messages or higher to the sys.stderr
671 console = logging.StreamHandler()
672 console.setLevel(logging.INFO)
673 # set a format which is simpler for console use
674 formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
675 # tell the handler to use this format
676 console.setFormatter(formatter)
677 # add the handler to the root logger
678 logging.getLogger('').addHandler(console)
681 def start_import(run_main_loop=True):
683 ImportWindow()
685 if run_main_loop:
686 setup_log()
688 gobject.set_application_name(_("Photo collection manager"))
689 gobject.set_prgname("marvin")
691 gtk.main()
693 def start():
696 setup_log()
697 log = logging.getLogger('view')
699 gobject.set_application_name(_("Photo collection manager"))
700 gobject.set_prgname("marvin")
702 wnd = MainWindow()
704 gtk.gdk.threads_enter()
705 gtk.main()
706 gtk.gdk.threads_leave()
709 if __name__ == '__main__':
710 start()