deleted blank line
[marvin.git] / marvin / view.py
blob7b8a3c10552effa45a244cca765f3e048adfab24
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
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 try:
41 import EXIF
42 except:
43 curr_dir = os.path.abspath(".")
44 sys.path.append(curr_dir)
45 import EXIF
47 ##logging system
48 import logging
49 log = None
51 ### l18n
52 APP_NAME = "marvin"
53 #Get the local directory since we are not installing anything
54 local_path = os.path.realpath(os.path.dirname(sys.argv[0]))
55 # Init the list of languages to support
56 langs = []
57 #Check the default locale
58 lc, encoding = locale.getdefaultlocale()
59 if (lc):
60 #If we have a default, it's the first in the list
61 langs = [lc]
62 # Now lets get all of the supported languages on the system
63 language = os.environ.get('LANGUAGE', None)
64 if (language):
65 langs += language.split(":")
67 langs += ["es_CL"]
69 gettext.bindtextdomain(APP_NAME, local_path)
70 gettext.textdomain(APP_NAME)
71 # Get the language to use
72 lang = gettext.translation(APP_NAME, local_path, languages=langs, fallback = True)
74 _ = lang.gettext
75 ###
77 gtk.gdk.threads_init()
78 wTree = None
80 from pkg_resources import resource_filename
83 class MainWindow (object):
85 def __init__ (self, **kwargs):
87 self._gladefile = resource_filename("marvin.ui", "marvin.glade")
88 global wTree
89 wTree = gtk.glade.XML(self._gladefile, "wnd_main")
91 signals = {
92 "on_entry_sql_filter_activate" : self._on_sql_filter_activate,
93 "on_btn_search_with_filter_clicked" : self._on_search_clicked,
94 "on_iconview1_selection_changed" : self._on_selection_changed,
95 "on_wnd_main_delete_event" : self._on_delete_event,
96 "on_iconview1_item_activated" : self._item_activated,
99 wTree.signal_autoconnect(signals)
101 list_photos = gtk.ListStore(Photo, gtk.gdk.Pixbuf, str)
102 list_photos.clear()
104 #setup the iconview and set the model
105 iconview = wTree.get_widget("iconview1")
106 iconview.set_model(list_photos)
107 iconview.set_pixbuf_column(1)
108 iconview.set_text_column(2)
110 self._win = wTree.get_widget("wnd_main")
111 uidef = resource_filename("marvin.ui", "bar.xml")
112 self._uimanager = gtk.UIManager()
113 accel_group = self._uimanager.get_accel_group()
114 self._win.add_accel_group(accel_group)
115 action_group = gtk.ActionGroup('my_actions')
116 action_group.add_actions([#TODO:finish actions
117 ('ImportPhoto', gtk.STOCK_ADD, _("Import Photos"), "<Control>i", None,
118 self._on_import_clicked),
121 self._uimanager.insert_action_group(action_group, 0)
122 self._uimanager.add_ui_from_file(uidef)
123 box = wTree.get_widget("vbox1")
124 toolbar = self._uimanager.get_widget("/toolbar")
125 box.pack_start(toolbar, False)
126 box.reorder_child(toolbar, 0)
128 self._win.show_all()
131 # prepare and launch the thread that fills the iconview model
132 thread = PhotoListThread(list_photos)
133 thread.start()
135 def _on_import_clicked(self, menuitem):
136 ImportWindow()
138 def _on_sql_filter_activate(self, entry):
139 button = wTree.get_widget("btn_search_with_filter")
140 button.clicked()
142 def _on_search_clicked(self, button):
145 Arguments:
146 - `self`:
147 - `button`:
149 model = wTree.get_widget("iconview1").get_model()
151 entry = wTree.get_widget("entry_sql_filter")
152 sql_filter = entry.get_text()
154 query = "select id, uri from photos " + sql_filter
155 thread = PhotoListThread(model, query)
156 thread.start()
158 def _on_selection_changed(self, iconview):
161 Arguments:
162 - `self`:
163 - `iconview`:
165 num_selec_items = len(iconview.get_selected_items())
166 if num_selec_items == 1:
168 lbl = wTree.get_widget("lbl_original_date")
170 model = iconview.get_model()
171 iter = model.get_iter(iconview.get_selected_items()[0])
172 photo = model.get(iter, 0)[0]
173 str_date = photo.original_date.strftime("%x")
174 lbl.set_text(_("Original Date %s" % str_date))
176 lbl = wTree.get_widget("lbl_tags")
178 lbl.set_text(str(photo.tags))
179 elif num_selec_items == 0:
180 wTree.get_widget("lbl_original_date").set_text("")
181 wTree.get_widget("lbl_tags").set_text("")
183 def _item_activated(self, iconview, path):
185 store = iconview.get_model()
186 iter = store.get_iter(path)
187 photo = store.get_value (iter, 0)
189 ImageWindow(photo, iconview)
191 def _on_delete_event (self, widget, event):
192 gtk.main_quit()
194 class ImageWindow:
196 def __init__(self, photo, iconview):
198 self.fullscreen = False
200 self._wTree = gtk.glade.XML(resource_filename("marvin.ui", "marvin.glade"), "wnd_display_img")
202 signals = {
203 "on_wnd_display_img_window_state_event": self._on_window_state_event,
204 "on_wnd_display_img_delete_event" : self._on_delete_event,
205 "on_wnd_display_img_key_press_event" : self._on_key_press,
207 self._wTree.signal_autoconnect (signals)
209 self.img_widget = self._wTree.get_widget("img")
211 self.iconview = iconview
212 self.photo = photo
213 self.path = gnomevfs.get_local_path_from_uri (self.photo.uri)
215 win = self._wTree.get_widget("wnd_display_img")
216 win.set_size_request (800, 600)
217 win.show_all()
219 self.update_image()
221 def _on_delete_event(self, widget, event):
224 Arguments:
225 - `self`:
226 - `widget`:
227 - `event`:
229 pass
231 def _on_window_state_event (self, widget, event):
232 "This method should be used for the fullscreen functinality"
233 pass
236 def _on_key_press(self, widget, event):
237 """Handle the key press event of the image window
239 Arguments:
240 - `self`:
241 - `widget`:
242 - `event`:
244 model = self.iconview.get_model()
246 iter = model.get_iter(self.iconview.get_selected_items()[0])
248 str_key = gtk.gdk.keyval_name(event.keyval)
250 if str_key == 'Right' or str_key == 'Down':
251 iter_next = model.iter_next(iter)
253 if iter_next == None:
254 iter_next = model.get_iter_first()
256 self.update_iconview(iter_next)
258 elif str_key == 'Left' or str_key == 'Up':
259 int_path = int(model.get_string_from_iter(iter)) - 1
260 if int_path < 0:
261 int_path = 0
262 str_path = str("%d" % (int_path))
264 iter_next = model.get_iter_from_string(str_path)
266 self.update_iconview(iter_next)
268 elif str_key == 'plus':
269 self.zoom(1.1)
271 elif str_key == 'minus':
272 self.zoom(0.9)
274 elif str_key == '1':
275 self.update_image(False)
277 elif str_key == 'F11':
278 win = self._wTree.get_widget("wnd_display_img")
279 if self.fullscreen:
280 win.unfullscreen()
281 self.fullscreen =False
282 else:
283 win.fullscreen()
284 self.fullscreen = True
286 elif str_key.lower() == 's':
287 self.update_image()
289 elif str_key == 'Escape':
290 win = self._wTree.get_widget("wnd_display_img")
291 win.destroy()
292 return
293 else:
294 print str_key
295 return
298 def zoom(self, factor):
300 curr_width = self.pixbuf.get_width()
301 curr_height = self.pixbuf.get_height()
302 scaled_pixbuf = self.pixbuf.scale_simple(int(curr_width*factor), int(curr_height*factor), gtk.gdk.INTERP_BILINEAR)
304 self.pixbuf = scaled_pixbuf
305 self.img_widget.set_from_pixbuf(self.pixbuf)
308 def update_iconview(self, iter_next):
310 self.photo = model.get(iter_next, 0)[0]
311 self.path = gnomevfs.get_local_path_from_uri (self.photo.uri)
313 self.update_image()
314 str_path = model.get_string_from_iter(iter_next)
315 self.iconview.select_path(str_path)
317 def update_image(self, to_wnd_size=True):
319 if (to_wnd_size):
320 win = self._wTree.get_widget("wnd_display_img")
321 size = win.get_size()
322 self.pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(self.path, size[0], size[1])
323 else:
324 self.pixbuf = gtk.gdk.pixbuf_new_from_file(self.path)
326 self.img_widget.set_from_pixbuf(self.pixbuf)
328 class PhotoListThread (threading.Thread):
330 def __init__ (self, model, query=None, use_progressbar=False):
331 super(PhotoListThread, self).__init__()
332 if query is not None:
333 self._query = query
334 else:
335 self._query = "select id, uri from photos order by time desc limit 10"
337 self._model = model
339 def run (self):
341 global wTree
343 progressbar = wTree.get_widget("progressbar1")
344 progressbar.show()
345 self._model.clear()
346 conn = connect_to_db()
347 c = conn.cursor()
348 c.execute (self._query)
350 for row in c:
351 i += 1
352 try:
353 p = Photo(row[0], row[1])
354 self._model.append([p,
355 p.pixbuf,
356 p.original_date.date().strftime("%x")])
357 except:
358 pass
360 progressbar.pulse()
362 Progressbar.push_message(_("%d pictures found.") % (i))
363 conn.close()
364 progressbar.hide()
366 class Progressbar(object):
368 @classmethod
369 def push_message(cls, msg, context="marvin"):
370 statusbar = wTree.get_widget("statusbar1")
371 context_id = statusbar.get_context_id(context)
372 msg_id = statusbar.push(context_id, msg)
373 gobject.timeout_add(10000,Progressbar.remove_from_statusbar,context_id, msg_id)
375 @classmethod
376 def remove_from_statusbar(cls, context_id, msg_id):
377 statusbar = wTree.get_widget("statusbar1")
378 statusbar.remove(context_id, msg_id)
380 return False #stop being called by gobject.timeout_add
383 class ImportWindow(object):
387 def __init__(self):
390 self._thread = None
391 glade_file = resource_filename("marvin.ui", "marvin.glade")
392 self._wTree = gtk.glade.XML(glade_file, "wnd_photo_import")
394 signals = {
395 "on_checkbtn_include_subdirs_toggled" : self._on_include_subdir_toggled,
396 "on_chooserbtn_dir_to_import_current_folder_changed" : self._on_folder_changed,
397 "on_btn_accept_clicked" : self._on_accept_clicked,
400 self._wTree.signal_autoconnect(signals)
402 list_photos = gtk.ListStore(Photo, gtk.gdk.Pixbuf, str)
403 list_photos.clear()
404 iconview = self._wTree.get_widget("iconview_preview")
405 iconview.set_model(list_photos)
406 iconview.set_pixbuf_column(1)
407 iconview.set_text_column(2)
409 win = self._wTree.get_widget("wnd_photo_import")
410 win.show_all()
412 def _on_folder_changed(self, widget):
413 self.refresh_iconview()
415 def _on_include_subdir_toggled(self, togglebutton):
416 self.refresh_iconview()
418 def refresh_iconview(self):
420 uris_list = list()
422 uri = self._wTree.get_widget("chooserbtn_dir_to_import").get_current_folder_uri()
423 selected_path= gnomevfs.get_local_path_from_uri(uri)
424 model = self._wTree.get_widget("iconview_preview").get_model()
426 recursive = self._wTree.get_widget("checkbtn_include_subdirs").get_active()
428 if recursive:
429 for root, dirs, files in os.walk(selected_path):
430 for name in files:
431 uri = "file://" + str(root) + "/" + str(name)
432 if gnomevfs.get_mime_type(uri) == "image/jpeg":
433 uris_list.append(uri)
434 else:
435 for name in os.listdir(selected_path):
436 uri = "file://" + str(selected_path) + "/" + str(name)
437 if gnomevfs.get_mime_type(uri) == "image/jpeg":
438 uris_list.append(uri)
440 if (self._thread is not None) and (not self._thread.is_stopped()):
441 self._thread.stop()
443 self._thread = PhotoPreviewThread(model, uris_list)
444 self._thread.start()
446 def _on_accept_clicked(self, button):
448 self._wTree.get_widget("wnd_photo_import").set_sensitive(False)
450 iconview = self._wTree.get_widget("iconview_preview")
451 model = iconview.get_model()
453 list_of_photos = list()
455 if len(iconview.get_selected_items()) == 0:
456 iter = model.get_iter_first()
458 while (iter is not None):
459 list_of_photos.append(model.get(iter, 0)[0])
460 iter = model.iter_next(iter)
462 else:
463 selected_items = iconview.get_selected_items()
464 for path in selected_items:
465 iter = model.get_iter(path)
466 list_of_photos.append(model.get(iter, 0)[0])
468 copy_to_photodir = self._wTree.get_widget("checkbtn_copy").get_active()
469 progressbar = self._wTree.get_widget("progressbar_import")
470 thread = ImportPhotoThread(list_of_photos, copy_to_photodir, progressbar)
471 thread.start()
473 class ImportPhotoThread(threading.Thread):
475 def __init__(self, list_of_photos, copy_to_photodir, progressbar):
476 super(ImportPhotoThread, self).__init__()
478 self._photos = list_of_photos
479 self._copy_to_photodir = copy_to_photodir
480 self._progressbar = progressbar
482 def run(self):
483 self._conn = connect_to_db()
484 self._c = self._conn.cursor()
486 self._progressbar.show()
487 i = 0
488 total = float(len(self._photos))
489 for photo in self._photos:
490 i += 1
491 self._import (photo, self._copy_to_photodir)
492 self._progressbar.set_fraction(i/total)
493 self._progressbar.set_text(_("Imported %d of %d") % (i, int(total)))
495 self._conn.commit()
496 self._conn.close()
498 win = self._progressbar.get_toplevel()
499 win.destroy()
501 def _import (self, photo, copy_to_photodir):
503 if copy_to_photodir:
504 new_uri = self._copy_photo(photo.uri)
505 else:
506 new_uri = photo.uri
508 self._c.execute("""insert into photos(uri, time, description, roll_id, default_version_id, rating)
509 values (?, ?, '', 0, 1, 0)""", (new_uri, time.mktime(photo.original_date.timetuple()),))
511 def _copy_photo(self, uri):
513 base = "/apps/f-spot/import"
514 client = gconf.client_get_default()
515 client.add_dir(base, gconf.CLIENT_PRELOAD_NONE)
516 root_photo_dir = client.get_string(base + "/storage_path")
518 src_path = gnomevfs.get_local_path_from_uri(uri)
519 f = open(src_path, 'rb')
520 tags = EXIF.process_file(f, stop_tag='DateTimeOriginal')
522 if tags.has_key('EXIF DateTimeOriginal'):
523 (aux_date, aux_time) = str(tags['EXIF DateTimeOriginal']).split(' ')
524 (year, month, day) = aux_date.split(':')
526 else:
527 mtime = time.localtime(os.path.getmtime(src_path))
528 year = str(mtime[0])
529 moth = str(mtime[1])
530 day = str(mtime[2])
533 destdir = os.path.join(root_photo_dir, year, month, day)
534 list_dir = (os.path.join(root_photo_dir, year),
535 os.path.join(root_photo_dir, year, month),
536 os.path.join(root_photo_dir, year, month, day))
537 for aux_path in list_dir:
538 if not os.path.exists(aux_path):
539 os.mkdir(aux_path)
541 if os.path.exists(os.path.join(destdir, os.path.basename(src_path))):
542 aux = os.path.basename(src_path).rsplit('.', 1)
543 suff = str(time.localtime()[3]) + str(time.localtime()[4])
544 destdir = os.path.join(destdir, aux[0] + suff + aux[1])
545 else:
546 destdir = os.path.join(destdir, os.path.basename(src_path))
548 shutil.copyfile(src_path, destdir)
550 return "file://" + destdir #returns the destination uri
552 class PhotoPreviewThread(threading.Thread):
554 def __init__(self, model, uris):
555 super(PhotoPreviewThread, self).__init__()
557 self._model = model
558 self._uris = uris
559 self._stop = False
560 self._stopped = True
562 def run(self):
563 self._stopped = False
564 self._model.clear()
566 for uri in self._uris:
567 if self._stop:
568 self._stopped = True
569 self._stop = False
570 return
572 photo = Photo(-1, uri, 150)
573 self._model.append([photo,
574 photo.pixbuf,
575 photo.original_date.date().strftime("%x")])
577 def stop(self):
578 self._stop = True
580 def is_stopped(self):
581 return self._stopped
583 def setup_log():
584 # code adapted from
585 # http://docs.python.org/lib/multiple-destinations.html example
586 # set up logging to file - see previous section for more details
587 logging.basicConfig(level=logging.DEBUG,
588 #format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
589 format='%(levelname)-8s %(message)s',
590 datefmt='%m-%d %H:%M')
591 # define a Handler which writes INFO messages or higher to the sys.stderr
592 console = logging.StreamHandler()
593 console.setLevel(logging.INFO)
594 # set a format which is simpler for console use
595 formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
596 # tell the handler to use this format
597 console.setFormatter(formatter)
598 # add the handler to the root logger
599 logging.getLogger('').addHandler(console)
602 def start_import(run_main_loop=True):
604 ImportWindow()
606 if run_main_loop:
607 setup_log()
609 gobject.set_application_name(_("Photo collection manager"))
610 gobject.set_prgname("marvin")
612 gtk.main()
614 def start():
617 setup_log()
618 log = logging.getLogger('view')
620 gobject.set_application_name(_("Photo collection manager"))
621 gobject.set_prgname("marvin")
623 wnd = MainWindow()
625 gtk.main()
628 if __name__ == '__main__':
629 start()