#!/usr/bin/env python # GPixPod - the free and open source way to manage photos on your POD! # Copyright (C) 2006 Flavio Gargiulo (FLAGAR.com) # # 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 2 # 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk, gtk.glade, sys, os, gobject, cPickle, webbrowser, re, locale, gettext if os.path.basename(sys.argv[0]) == 'gpixpod.py': if os.path.dirname(sys.argv[0]) != '': os.chdir(os.path.dirname(sys.argv[0])) from mh import * from utils import * # Internationalization #locale.setlocale(locale.LC_ALL, 'it_IT.UTF-8') locale.setlocale(locale.LC_ALL, '') gettext.bindtextdomain('gpixpod', 'po') gettext.textdomain('gpixpod') gettext.install('gpixpod', 'po', True) gtk.glade.bindtextdomain('gpixpod', 'po') try: from ipodhal import * except ImportError: print _("iPod HAL autodetection disabled") class GPixPod: """ Main GUI interface """ def __init__(self): """ Initialize variables and the main window """ # Preferences self.homedir = os.path.expanduser('~') self.preferencesdir = os.path.join(self.homedir, '.gpixpod') if not os.path.isdir(self.preferencesdir): os.mkdir(self.preferencesdir) self.preferencesfile = os.path.join(self.preferencesdir, 'config') # Default preferences self.prefs = self.defaultprefs = {'ipod_autodetect':True, 'ipod_mountpoint':"/mnt/ipod", 'ipod_askbeforeopen':True, 'photo_copyfullres':True, 'path_lastphotodb':self.homedir, 'path_lastimages':self.homedir, 'path_lastsavedimages':self.homedir} # Overriding default preferences try: self.LoadPreferences() except IOError: print _("Using default preferences") # Building the GUI self.win_callbacks = {'on_addalbum1_activate':(self.ShowGetLine, self.AddAlbum, _('Add new photo album'), _('Enter the name of the new album:'), _('New Photo Album')), 'on_addalbumbutton_clicked':(self.ShowGetLine, self.AddAlbum, _('Add new photo album'), _('Enter the name of the new album:'), _('New Photo Album')), 'on_addphoto1_activate':(self.ShowChooser, self.AddPhoto, _('Images'), 'PIXBUF', True, self.prefs['path_lastimages'], True), 'on_addphotobutton_clicked':(self.ShowChooser, self.AddPhoto, _('Images'), 'PIXBUF', True, self.prefs['path_lastimages'], True), 'on_donate1_activate':(self.LaunchBrowser, 'https://www.paypal.com/xclick/business=flagar%40gmail.com&item_name=Helping+GPixPod+development&no_shipping=1&tax=0¤cy_code=EUR&lc=US'), 'on_about1_activate':self.ShowAbout, 'on_delete1_activate':self.Delete, 'on_aboutbutton_clicked':self.ShowAbout, 'on_deletebutton_clicked':self.Delete, 'on_preferences1_activate':self.ShowPreferences, 'on_eject1_activate':self.Eject, 'on_ejectbutton_clicked':self.Eject, 'on_quit1_activate':self.Quit, 'on_window1_destroy':self.Quit, 'on_quitbutton_clicked':self.Quit, 'on_window1_destroy':self.Quit, 'on_open1_activate':(self.ShowChooser, self.Open, 'iPod Photo Database', 'Photo Database', False, self.prefs['path_lastphotodb']), 'on_openbutton_clicked':(self.ShowChooser, self.Open, 'iPod Photo Database', 'Photo Database', False, self.prefs['path_lastphotodb']), 'on_new1_activate':(self.ShowChooser, self.CreateNew, None, None, False, None, False, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, _('Select the iPod folder mountpoint')), 'on_treeview1_row_activated':self.DetailsDoubleClick, 'on_treeview1_cursor_changed':self.Details, 'on_save1_activate':self.Save, 'on_savebutton_clicked':self.Save, 'on_treeview1_unselect_all':self.HideActions, 'on_window1_state_changed':self.Opening, 'on_spinbutton1_value_changed':self.ZoomImage, 'on_saveimagebutton_clicked':(self.ShowChooser, self.SaveImage, _('Images'), 'PIXBUF', False, self.prefs['path_lastsavedimages'], False, gtk.FILE_CHOOSER_ACTION_SAVE, _('Save image')), 'on_cleancache1_activate':self.CleanCache} self.win = gtk.glade.XML('gpixpod.glade', 'window1', 'gpixpod') self.win.signal_autoconnect(self.win_callbacks) self.window = self.win.get_widget('window1') self.treeview = self.win.get_widget('treeview1') self.label = self.win.get_widget('label1') self.label.set_line_wrap(True) self.image = self.win.get_widget('image1') self.savefilename = None self.imagehbox = self.win.get_widget('imagehbox') self.imagehbox.hide() self.spinbutton = self.win.get_widget('spinbutton1') self.statusbar = self.win.get_widget('statusbar1') self.statusbar_context = self.statusbar.get_context_id('GPixPod') self.statusbar.push(self.statusbar_context, _('Please connect the iPod...')) self.progressbar = self.win.get_widget('progressbar1') self.progresslabel = self.win.get_widget('label3') #self.current_album = 0 # Lists to mark the new changes self.albums_to_add = [] self.albums_to_delete = [] self.albums_to_rename = [] # the entry is the corresponding tuple of album ID and new name self.photos_to_delete = [] self.photos_to_add = [] # the entry is the corresponding tuple of filename path and parent album ID # Setting sensitiveness self.SetSensitive(('save1', 'savebutton', 'addalbum1', 'addalbumbutton', 'eject1', 'ejectbutton', 'spinbutton1', 'saveimagebutton'), False) self.HideActions(None) # hide "Add Photo" and "Delete" by default self.win.get_widget('cleancache1').set_sensitive(False) self.DB = None # Used to check if a Photo Database is loaded if sys.modules.has_key('ipodhal') and self.prefs['ipod_autodetect']: # If ipodhal.py has been imported successfully (python-dbus/hal/gnomevfs are available), gobject.idle_add(self.HALdetect) # Checking in a loop events of connection and removal of an iPod else: self.Opening(None) def SetSensitive(self, widget_list, state): """ Change sensitivity in the same way for a group of widgets """ for widget_name in widget_list: self.win.get_widget(widget_name).set_sensitive(state) def HALdetect(self): """ Detect using HAL """ if sys.modules.has_key('ipodhal') and self.prefs['ipod_autodetect']: #ipodHal will connect to HAL via DBUS #and 2 signal are implemented to detect ipod ipod_hal = iPodHal() #Connect to some signal ipod_hal.connect("ipod-added", self.OnIpodConnect) ipod_hal.connect("ipod-removed", self.OnIpodDisconnect) #Launch the detection #if ipod already present signal 'ipod-added' is emit now ipod_hal.start_monitor() def Opening(self, widget): """ Prepare to open the Photo Database, trying to find it and evaluating command line argument """ #if len(sys.argv) > 1: # self.DBOpen(sys.argv[1]) #else: standard_path = self.prefs['ipod_mountpoint'] standard_loc = os.path.join(standard_path, 'Photos', 'Photo Database') if os.path.isfile(standard_loc): if self.prefs['ipod_askbeforeopen'] == False or self.Ask(_('<b>Photo Database</b> found.'), _('Do you want to load the Photo Database found in the <b>iPod</b> attached to <b><i>%s</i></b>?') % standard_path): self.DBOpen(standard_loc) else: self.ShowChooser(None, self.Open, 'iPod Photo Database', 'Photo Database', False, self.prefs['path_lastphotodb']) elif os.path.isdir(os.path.join(standard_path, 'iPod_Control')): if self.prefs['ipod_askbeforeopen'] == False or self.Ask(_('<b>iPod</b> found, attached to <b><i>%s</i></b>.') % standard_path, _('But Photo Database <b>not found</b>. Do you want to create a new one?')): self.DBOpen(standard_loc) else: self.ShowChooser(None, self.Open, 'iPod Photo Database', 'Photo Database', False, self.prefs['path_lastphotodb']) else: self.ShowChooser(None, self.Open, 'iPod Photo Database', 'Photo Database', False, self.prefs['path_lastphotodb']) def ToSave(self, unsaved=True): """ Mark the Photo Database to be saved, new changes are happened. """ if unsaved: if self.window.get_title()[0] != '*': # Already marked as unsaved, thus we avoid multiple alterisks! ;-) self.window.set_title('*%s' % self.window.get_title()) self.win.get_widget('save1').set_sensitive(True) self.win.get_widget('savebutton').set_sensitive(True) else: self.window.set_title(self.window.get_title()[1:]) self.win.get_widget('save1').set_sensitive(False) self.win.get_widget('savebutton').set_sensitive(False) def Open(self, widget): # To be called from signal of the file chooser """ Let select the Photo Database by file selection """ filename = self.filechooser.get_filename() self.filechooser.destroy() self.DBOpen(filename) def CreateNew(self, widget): """ Create a new Photo Database from scratch, after got the iPod mountpoint from the user """ mountpoint = self.filechooser.get_filename() self.filechooser.destroy() filename = os.path.join(mountpoint, 'Photos', 'Photo Database') self.DBOpen(filename) def CheckIpodModel(self, photodb_filename): """ Check for a supported iPod model """ ipod_mountpoint = photodb_filename.replace(os.path.join('', 'Photos', 'Photo Database'), '') ipod_model = getIpodModel(ipod_mountpoint) if ipod_model == '5G' or ipod_model == 'Nano' or ipod_model == 'Photo': pass elif ipod_model == 'Color': self.Say(_('You are using an <b>iPod %s</b>. Your model is considered as an iPod Photo and nothing is guaranteed to work.') % ipod_model, _('<b>Create backups before.</b> You have been warned! And please, report your feedback!'), gtk.MESSAGE_WARNING) else: self.Say(_('You are using an <b>iPod %s</b>. Please <b>quit now</b>: your model is not supported, and it never will be.') % ipod_model, _('It does not have a color screen and it does not support photos!'), gtk.MESSAGE_ERROR) if self.Ask(_('<b>Quit now?</b>')): gtk.main_quit() sys.exit() def ToggleProgress(self): """ Show/hide progress bar and related label at the bottom of the main window """ if self.progressbar.get_property('visible'): self.progressbar.hide() self.progresslabel.hide() else: self.progressbar.show() self.progresslabel.show() def DBOpen(self, filename): """ Actually open the Photo Database and process it """ self.CheckIpodModel(filename) # The while loop is needed to update the GTK widgets. # The GTK interface is event-driven, so functions and code from outside the mainloop # will prevent to properly update widgets. # The while loop below together with another one, "while gtk.events_pending()" and # the initialization of another gtk.main_iteration() seems to solve everything. work_left = True while work_left: self.statusbar.push(self.statusbar_context, _('Opening %s...') % filename) self.ToggleProgress() self.progresslabel.set_text(_('Opening Photo Database... It could take several minutes, depending on its size!')) while gtk.events_pending(): gtk.main_iteration() self.DB = MH(filename) # Actually opening and processing Photo Database self.progressbar.set_fraction(0.5) while gtk.events_pending(): gtk.main_iteration() if len(self.treeview.get_columns()) > 0: self.treeview.remove_column(self.column) self.Populate() self.window.set_title('%s - GPixPod' % filename) self.win.get_widget('addalbum1').set_sensitive(True) self.win.get_widget('addalbumbutton').set_sensitive(True) self.win.get_widget('cleancache1').set_sensitive(True) self.progressbar.set_fraction(1) while gtk.events_pending(): gtk.main_iteration() self.ToggleProgress() self.statusbar.push(self.statusbar_context, _('Photo Database %s opened.') % filename) while gtk.events_pending(): gtk.main_iteration() work_left = False self.prefs['path_lastphotodb'] = self.DB.dbdir # Remembering the directory of the last opened Photo Database def Save(self, widget): """ Save the changes to the Photo Database """ # The while loop is needed to update the GTK widgets. # The GTK interface is event-driven, so functions and code from outside the mainloop # will prevent to properly update widgets. # The while loop below together with another one, "while gtk.events_pending()" and # the initialization of another gtk.main_iteration() seems to solve everything. work_left = True while work_left: self.ToggleProgress() self.SetSensitive(('treeview1', 'open1', 'openbutton', 'save1', 'savebutton', 'addalbum1', 'addalbumbutton', 'addphoto1', 'addphotobutton', 'delete1', 'deletebutton', 'cleancache1'), False) albums_to_add_len = len(self.albums_to_add) self.progresslabel.set_text(_('Adding albums...')) self.progressbar.set_fraction(0) while gtk.events_pending(): gtk.main_iteration() for x in range(0, albums_to_add_len): # Adding photo albums self.DB.AddAlbum(self.albums_to_add[x]) self.progressbar.set_fraction((x + 1.0)/albums_to_add_len) while gtk.events_pending(): gtk.main_iteration() self.albums_to_add = [] # Reset albums_to_rename_len = len(self.albums_to_rename) self.progresslabel.set_text(_('Renaming albums...')) while gtk.events_pending(): gtk.main_iteration() for x in range(0, albums_to_rename_len): # Renaming photo albums self.DB.RenameAlbum(*self.albums_to_rename[x]) self.progressbar.set_fraction((x + 1.0)/albums_to_rename_len) while gtk.events_pending(): gtk.main_iteration() self.albums_to_rename = [] # Reset photos_to_add_len = len(self.photos_to_add) self.progresslabel.set_text(_('Adding photos...')) self.progressbar.set_fraction(0) while gtk.events_pending(): gtk.main_iteration() for x in range(0, photos_to_add_len): # Adding photos self.DB.AddPhoto(*self.photos_to_add[x]) self.progressbar.set_fraction((x + 1.0)/photos_to_add_len) self.progressbar.set_text(_('%s of %s - %s%%') % (x+1, photos_to_add_len, 100*(x+1)/photos_to_add_len)) while gtk.events_pending(): gtk.main_iteration() self.photos_to_add = [] # Reset photos_to_delete_len = len(self.photos_to_delete) self.progresslabel.set_text(_('Deleting photos...')) self.progressbar.set_fraction(0) while gtk.events_pending(): gtk.main_iteration() for x in range(0, photos_to_delete_len): # Deleting photos self.DB.RemovePhoto(self.photos_to_delete[x]) self.progressbar.set_fraction((x + 1.0)/photos_to_delete_len) self.progressbar.set_text(_('%s of %s - %s%%') % (x+1, photos_to_delete_len, 100*(x+1)/photos_to_delete_len)) while gtk.events_pending(): gtk.main_iteration() self.photos_to_delete = [] # Reset albums_to_delete_len = len(self.albums_to_delete) self.progresslabel.set_text(_('Deleting albums...')) self.progressbar.set_fraction(0) while gtk.events_pending(): gtk.main_iteration() for x in range(0, albums_to_delete_len): # Deleting photo albums self.DB.RemoveAlbumAndPhotos(self.albums_to_delete[x]) self.progressbar.set_fraction((x + 1.0)/albums_to_delete_len) self.progressbar.set_text(_('%s of %s - %s%%') % (x+1, albums_to_delete_len, 100*(x+1)/albums_to_delete_len)) while gtk.events_pending(): gtk.main_iteration() self.albums_to_delete = [] # Reset self.progresslabel.set_text(_('Saving Photo Database...')) self.progressbar.set_text(_('Please wait... (it could take several minutes!)')) while gtk.events_pending(): gtk.main_iteration() self.DB.Save() # Actually saving the new-generated Photo Database file self.ToSave(False) # Marking as saved self.statusbar.push(self.statusbar_context, _('Photo Database saved.')) self.DBOpen(self.DB.dbname) # Re-opening the Photo Database to let user eventually continue work self.SetSensitive(('treeview1', 'open1', 'openbutton', 'addalbum1', 'addalbumbutton', 'cleancache1'), True) self.ToggleProgress() while gtk.events_pending(): gtk.main_iteration() work_left = False def GetDBLists(self): # Probably not very useful. Maybe to delete, after merging in Populate() and checking all other possible dependencies. """ Refresh list variables for photo albums and photos """ self.albums = self.DB.Albums() self.photos = self.DB.Photos() def Populate(self): """ Fill the tree view with the Photo Database contents """ self.tree = gtk.TreeStore(str) self.GetDBLists() for album in self.albums[1:]: albumroot = self.tree.append(None, ['<b>%s</b>' % album.name]) for pic in album.pics: self.tree.append(albumroot, ['%s. %s' % (str(pic), self.photos[pic-100].fullres.split(':')[-1])]) self.treeview.set_model(self.tree) self.cell = gtk.CellRendererText() #self.cell.set_property('editable', True) #self.cell.connect('edited', self.Rename) self.column = gtk.TreeViewColumn(_('Photo Albums'), self.cell, markup=0) self.treeview.append_column(self.column) # Enable Drag & Drop (DND) for the treeview targets = [('text/plain', 0, 1), ('TEXT', 0, 2), ('STRING', 0, 3)] # Drag outside not working and not so much useful: to delete # targets2 = gtk.target_list_add_image_targets() # targets2 = gtk.target_list_add_text_targets(targets2) # self.treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, targets2, gtk.gdk.ACTION_COPY) self.treeview.enable_model_drag_dest(targets, gtk.gdk.ACTION_COPY) # self.treeview.connect("drag_data_get", self.TreeviewDrag) self.treeview.connect("drag_data_received", self.TreeviewDrop) def Details(self, widget): #, path, column): """ React to single-click on the tree view, displaying the details if a photo is selected. """ self.ShowActions(None) item_selected = self.treeview.get_selection().get_selected()[1] path = self.tree.get_path(item_selected) if len(path) > 1: # We are interacting with an image. self.imagehbox.show() album = None if len(self.albums) > (path[0]+1): album = self.albums[path[0]+1] if album != None and len(album.pics) > path[1] and self.photos_to_add.count(album.pics[path[1]]) == 0 and self.photos_to_delete.count(album.pics[path[1]]) == 0: # Not added now, the image was already there; neither deleted. fullres = '%s' % self.photos[album.pics[path[1]]-100].fullres.replace('\000', '') # Removing null characters photofile = os.path.join(self.DB.dbdir, *fullres.split(':')[1:]) # Converting from iPod (Mac) relative path to absolute native path self.ShowImage(photofile, album.name, album.pics[path[1]]) else: # The image has been just added self.ClearDetails() itemtext = self.tree[(path[0], path[1])][0] itemdot = itemtext.find('.') for new_photo in self.photos_to_add: if new_photo[1] == (path[0]+1) and os.path.basename(new_photo[0]) == itemtext[itemdot+2:]: self.ShowImage(new_photo[0], self.tree[path[0]][0], itemtext[:itemdot], False) break #self.label.set_text(_('Image has been just added. Its details will be available after saved.')) else: # We are interacting with an album. self.ClearDetails() if self.treeview.row_expanded(path): # The album row is already expanded, so we collapse it. self.treeview.collapse_row(path) else: self.treeview.expand_to_path(path) def ShowImage(self, photofile, album_name, photo_id, saved=True): """ Show image in the details pane on the right """ if os.path.isfile(photofile): self.details_photofile = photofile self.savefilename = os.path.basename(photofile) self.SetSensitive(('spinbutton1', 'saveimagebutton'), True) pixbuf = gtk.gdk.pixbuf_new_from_file(photofile) width = pixbuf.get_width() height = pixbuf.get_height() if saved: imgstatuscolor = '#009900' imgstatusname = _('Saved') else: imgstatuscolor = '#990000' imgstatusname = _('Not saved yet') self.label.set_markup(_('Photo Album: <b>%s</b> \nPhoto ID: <b>%s</b> \nPhoto path: <b>%s</b> \nOriginal size: <b>%sx%s</b> \nStatus: <b><span foreground="%s">%s</span></b>') % (album_name, photo_id, photofile, width, height, imgstatuscolor, imgstatusname)) win_width, win_height = self.window.get_size() ratio_width, ratio_height = getRatioSize(width, height, win_width-316, win_height-196) pixbuf = pixbuf.scale_simple(ratio_width, ratio_height, gtk.gdk.INTERP_BILINEAR) # To update, offering the possibility to change the zoom level self.image.set_from_pixbuf(pixbuf) self.spinbutton.set_value(ratio_width*100/width) #self.current_album = path[0]+1 else: self.details_photofile = self.savefilename = None self.ClearDetails() self.label.set_text(_('Full resolution photo not found. Maybe have you deleted it?')) def SaveImage(self, widget): """ Save the image currently displayed in the details pane on the right """ if self.details_photofile != None: photosavename = self.filechooser.get_filename() self.filechooser.destroy() shutil.copy(self.details_photofile, photosavename) self.prefs['path_lastsavedimages'] = os.path.dirname(photosavename) def ZoomImage(self, widget): """ Change the zoom of the image currently displayed in the details pane on the right """ if self.details_photofile != None: pixbuf = gtk.gdk.pixbuf_new_from_file(self.details_photofile) new_width = int(pixbuf.get_width()*self.spinbutton.get_value()/100) new_height = int(pixbuf.get_height()*self.spinbutton.get_value()/100) pixbuf = pixbuf.scale_simple(new_width, new_height, gtk.gdk.INTERP_BILINEAR) self.image.set_from_pixbuf(pixbuf) def DetailsDoubleClick(self, widget, path, column): """ React to double-click on the tree view, asking to rename if a photo album is selected. """ if len(path) == 1: # The double-click occurred on an album self.ShowGetLine(None, self.RenameAlbum, _('Rename photo album'), _('Enter the new name for the album "%s"') % self.tree[path[0]][0], self.tree[path[0]][0][3:-4]) def ClearDetails(self): """ Clear the displayed photo details (right pane) with default values """ self.image.set_from_pixbuf(None) self.label.set_text('') self.SetSensitive(('spinbutton1', 'saveimagebutton'), False) def Eject(self, widget): """ Eject the connected detected iPod """ ejecting_i, ejecting_o = os.popen4('eject %s' % self.ipod_mountpoint) ejecting_i.close() ejecting_result = ejecting_o.read() if ejecting_result == 'eject: unable to eject, last error: Invalid argument\n': self.Say(_('<b>iPod ejected</b>, but the <i>Do not disconnect</i> message seems to be still shown on the iPod.'), '%s %s' % (_('To let disappear the message from the iPod, make your <i>eject</i> executable <i>setuid root</i>'), _('(<tt>chmod +s `which eject`</tt> as <i>root</i>)'))) ejecting_o.close() def Quit(self, widget): """ Manage quitting, asking to save if there are unsaved changes. """ if self.win.get_widget('save1').get_property('sensitive'): if self.Ask(_('There are changes <b>not</b> saved.'), _('Do you want to save before quit?')): self.Save(None) self.WritePreferences() gtk.main_quit() def ShowGetLine(self, widget, ok_function, title=None, message=None, entrytext=None): """ Show a dialog to let enter a new line (e.g. to add a new album, or to renaming an existing one) """ self.getline = gtk.glade.XML('gpixpod.glade', 'dialog2', 'gpixpod') self.getlineinput = self.getline.get_widget('dialog2') self.getlineinput.set_icon_from_file('GPixPod_icon.png') self.getlineinput.set_default_response(gtk.RESPONSE_OK) self.getline_callbacks = {'on_cancelbutton1_clicked':self.DestroyGetLine, 'on_okbutton1_clicked':ok_function} self.getline.signal_autoconnect(self.getline_callbacks) if title != None: self.getlineinput.set_title(title) if message != None: label = self.getline.get_widget('label2') label.set_markup(message) if entrytext != None: entry = self.getline.get_widget('entry1') entry.set_text(entrytext) entry.select_region(0, -1) def DestroyGetLine(self, widget): """ Actually destroy the GetLine dialog when receiving the destroy signal """ self.getlineinput.destroy() def ShowChooser(self, widget, ok_function, filter_name=None, pattern=None, multiple=False, path=None, preview=False, chooser_type=None, title=None): """ Show file chooser, to select and open file based on the pattern specified, passing to the specified function """ self.chooser_callbacks = {'on_button1_clicked':self.DestroyChooser, 'on_button2_clicked':ok_function} self.chooser = gtk.glade.XML('gpixpod.glade', 'filechooserdialog1', 'gpixpod') self.chooser.signal_autoconnect(self.chooser_callbacks) self.filechooser = self.chooser.get_widget('filechooserdialog1') self.filechooser.set_select_multiple(multiple) if path != None: self.filechooser.set_current_folder(path) if filter_name != None and pattern != None: filter = gtk.FileFilter() filter.set_name(filter_name) if pattern == 'PIXBUF': filter.add_pixbuf_formats() else: filter.add_pattern(pattern) self.filechooser.add_filter(filter) if chooser_type != None: self.filechooser.set_action(chooser_type) if chooser_type == gtk.FILE_CHOOSER_ACTION_SAVE: self.filechooser.set_do_overwrite_confirmation(True) if self.savefilename != None: self.filechooser.set_current_name(self.savefilename) if title != None: self.filechooser.set_title(title) if preview: self.chooser.signal_connect('on_filechooserdialog1_selection_changed', self.ChooserUpdatePreview) self.filechooser_previewbox = gtk.VBox() self.filechooser_previewbox.set_size_request(200, 200) self.filechooser_preview = gtk.Image() self.filechooser_previewlabel = gtk.Label() self.filechooser_previewbox.pack_start(self.filechooser_preview) self.filechooser_previewbox.pack_start(self.filechooser_previewlabel) self.filechooser.set_preview_widget(self.filechooser_previewbox) self.filechooser_previewbox.show_all() def ChooserUpdatePreview(self, widget): """ Update the preview widget set in the file chooser on change of selection """ preview_filename = self.filechooser.get_preview_filename() if preview_filename != None and os.path.isfile(preview_filename): self.filechooser.set_preview_widget_active(True) pixbuf = gtk.gdk.pixbuf_new_from_file(preview_filename) # Calculating proper width/height with ratio within limits new_width = width = pixbuf.get_width() new_height = height = pixbuf.get_height() max_width = 200 max_height = 200 if new_width > max_width or new_height > max_height: new_width = max_width new_height = max_width*height/width if new_height > max_height: new_height = max_height new_width = max_height*width/height pixbuf = pixbuf.scale_simple(new_width, new_height, gtk.gdk.INTERP_TILES) # INTER_TILES is a bit faster, since quality is not needed here self.filechooser_preview.set_from_pixbuf(pixbuf) preview_info = gtk.gdk.pixbuf_get_file_info(preview_filename) # a tuple: pixbuf dictionary, width, height preview_stat = os.stat(preview_filename) preview_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(preview_stat[8])) # Last modified date of the image file preview_size = preview_stat[6]/1024 # Size in Kb self.filechooser_previewlabel.set_markup('<i>%s, %sx%s, %s KB</i>\n%s' % (preview_info[0]['name'], width, height, preview_size, preview_date)) else: self.filechooser.set_preview_widget_active(False) def DestroyChooser(self, widget): """ Actually destroy the FileChooser dialog when receiving the destroy signal """ self.filechooser.destroy() def AddAlbum(self, widget): """ Add a new album to the tree view, marking to actually add it when saving """ album_name = self.getline.get_widget('entry1').get_text() self.getlineinput.destroy() self.albums_to_add.append(album_name) # Mark the new album to be added when saving #self.GetDBLists() self.tree.append(None, ['<b>%s</b>' % album_name]) # Adding the new album to the tree view self.ToSave() # Marking as unsaved def RenameAlbum(self, widget): """ Rename an album in the tree view, marking to actually renaming it when saving """ new_album_name = self.getline.get_widget('entry1').get_text() self.getlineinput.destroy() item_selected = self.treeview.get_selection().get_selected()[1] path_selected = self.tree.get_path(item_selected) self.albums_to_rename.append((path_selected[0]+1, new_album_name)) # Mark the album to be actually renamed when saving self.tree[path_selected[0]][0] = '<b>%s</b>' % new_album_name # Updating tree view row self.ToSave() # Marking as unsaved def AddPhoto(self, widget, filenames=None): # Third argument added to let the function being called by DND """ Add a new photo to the tree view, marking to actually add it when saving """ item_selected = self.treeview.get_selection().get_selected()[1] path_selected = self.tree.get_path(item_selected) album_selected = self.tree.get_iter((path_selected[0],)) # The parent album is obtained from the first element of the path if filenames == None: filenames = self.filechooser.get_filenames() self.filechooser.destroy() for filename in filenames: if os.path.isfile(filename): new_photo_id = len(self.photos)+99+len(self.photos_to_add) self.photos_to_add.append((filename, path_selected[0]+1)) # Mark the new photo to be actually added when saving #self.GetDBLists() # Or completely rePopulate? # Adding the new photo to the tree view self.tree.append(album_selected, ['%s. %s' % (new_photo_id, os.path.basename(filename))]) self.ToSave() # Marking as unsaved self.prefs['path_lastimages'] = os.path.dirname(filename) # Remembering last images directory def Delete(self, widget): """ Delete either a photo or a whole album from the tree view, marking to actually delete it when saving """ item_selected = self.treeview.get_selection().get_selected()[1] path_selected = self.tree.get_path(item_selected) if len(path_selected) == 1: # then we are deleting an album if self.Ask(_('Are you sure to delete the whole album?')): self.tree.remove(item_selected) # Removing the photo album and its children from tree view self.albums_to_delete.append(path_selected[0]+1) # Marking the album to be deleted self.ToSave() # Marking as unsaved else: # then the request of deletion is for a photo # Getting the photo id from the content of the row of the tree view, the first part (before the first dot, see the row in the GUI) photo_id = int(self.tree.get_value(item_selected, 0).split('.')[0]) self.photos_to_delete.append(photo_id) # Marking the photo to be actually deleted when saving self.tree.remove(item_selected) # Removing the photo from the tree view self.ToSave() # Marking as unsaved self.ClearDetails() # We clear the image details (right pane) self.HideActions(None) # After deleting, we lose the selection: thus we should at least hide the related actions. # not working yet and to delete # def TreeviewDrag(self, treeview, context, selection, target_id, etime): # """ Manage dragging out from the treeview """ # treeselection = treeview.get_selection() # model, iter = treeselection.get_selected() # path = model.get_path(iter) # data = model.get_value(iter, 0) # #selection.set(selection.target, 8, data) # album = self.albums[path[0]+1] # fullres = '%s' % self.photos[album.pics[path[1]]-100].fullres.replace('\000', '') # Removing null characters # photofile = os.path.join(self.DB.dbdir, *fullres.split(':')[1:]) # Converting from iPod (Mac) relative path to absolute native path # print photofile # if os.path.isfile(photofile): # pixbuf = gtk.gdk.pixbuf_new_from_file(photofile) # selection.set('image/pixbuf', 8, pixbuf) def TreeviewDrop(self, treeview, context, x, y, selection, info, etime): """ Manage dropping inside the treeview """ model = treeview.get_model() sel = treeview.get_selection() data = selection.data data = data.replace('file://', '') data = data.split() drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info sel.select_path(path[:1]) else: sel.select_path((len(model)-1)) data2 = [] for filename in data: filenamepart, ext = os.path.splitext(filename) if re.match('[jJ][pP][eE]?[gG]|[pP][nN][gG]|[sS][vV][gG]|[wW][bB][mM][pP]|[wW][mM][fF]|[bB][mM][pP]|[gG][iI][fF]|[tT][iI][fF][fF]|[xX][pP][mM]', ext[1:]) != None: if gtk.gdk.pixbuf_get_file_info(filename) != None: data2.append(filename) if len(data) > 0: self.AddPhoto(None, data2) # if context.action == gtk.gdk.ACTION_MOVE: # context.finish(True, True, etime) # return def ShowActions(self, widget): """ Show "Add Photo" and "Delete" actions (to be called when a treeview item is selected) """ self.win.get_widget('addphoto1').set_sensitive(True) self.win.get_widget('addphotobutton').set_sensitive(True) self.win.get_widget('delete1').set_sensitive(True) self.win.get_widget('deletebutton').set_sensitive(True) def HideActions(self, widget): """ Hide "Add Photo" and "Delete" actions (to be called when no treeview items are selected) """ self.win.get_widget('addphoto1').set_sensitive(False) self.win.get_widget('addphotobutton').set_sensitive(False) self.win.get_widget('delete1').set_sensitive(False) self.win.get_widget('deletebutton').set_sensitive(False) def CleanCache(self, widget): # More useful for development purposes than for users, maybe to be moved away """ Clean the single thumbnails cache """ if self.Ask(_('<b>Are you sure to remove thumbnails cache?</b>'), '%s%s' % (_('Thumbnails cache is automatically removed when saving, only in rare cases you would remove it manually.\n'), _('It is recommended to <b>cancel</b> right away, unless you know what are you doing.'))): self.DB.RemoveThumbsCache() # Actually removing cache self.win.get_widget('cleancache1').set_sensitive(False) self.Say(_('Thumbnails cache removed!')) def ShowAbout(self, widget): """ Show the about dialog """ gtk.about_dialog_set_url_hook(self.LaunchBrowser) self.about = gtk.glade.XML('gpixpod.glade', 'aboutdialog1', 'gpixpod') self.aboutdlg = self.about.get_widget('aboutdialog1') self.aboutdlg.set_icon_from_file('GPixPod_icon.png') def ShowPreferences(self, widget): """ Show the preferences dialog """ self.preferences = gtk.glade.XML('gpixpod.glade', 'preferencesdialog', 'gpixpod') self.preferencesdlg = self.preferences.get_widget('preferencesdialog') self.preferences_callbacks = {'on_cancelbutton2_clicked':self.DestroyPreferences, 'on_okbutton2_clicked':self.StorePreferences, 'on_defaultsbutton_clicked':self.RestoreDefaultPreferences} self.preferences.signal_autoconnect(self.preferences_callbacks) self.LoadDefaultPreferences() def LoadDefaultPreferences(self): """ Load the default preferences """ self.preferences.get_widget('autodetectcheckbutton').set_active(self.prefs['ipod_autodetect']) self.preferences.get_widget('copyfullrescheckbutton').set_active(self.prefs['photo_copyfullres']) self.preferences.get_widget('askbeforeopencheckbutton').set_active(self.prefs['ipod_askbeforeopen']) self.preferences.get_widget('impfilechooserbutton').set_current_folder(self.prefs['ipod_mountpoint']) def RestoreDefaultPreferences(self, widget): """ Restore the default preferences """ self.prefs = self.defaultprefs self.LoadDefaultPreferences() def DestroyPreferences(self, widget): """ Destroy the preferences dialog after received the destroy signal """ self.preferencesdlg.destroy() def StorePreferences(self, widget): """ Store the preferences """ self.prefs['ipod_autodetect'] = self.preferences.get_widget('autodetectcheckbutton').get_active() self.prefs['photo_copyfullres'] = self.preferences.get_widget('copyfullrescheckbutton').get_active() self.prefs['ipod_mountpoint'] = self.preferences.get_widget('impfilechooserbutton').get_current_folder() self.prefs['ipod_askbeforeopen'] = self.preferences.get_widget('askbeforeopencheckbutton').get_active() self.WritePreferences() self.LoadPreferences() self.preferencesdlg.destroy() def WritePreferences(self): """ Write the preferences on file """ prf = open(self.preferencesfile, 'w+') cPickle.dump(self.prefs, prf) prf.close() def LoadPreferences(self): """ Load the preferences """ prf = open(self.preferencesfile, 'r') loadedprefs = cPickle.load(prf) # Overriding default preferences for prefkey, prefvalue in loadedprefs.iteritems(): self.prefs[prefkey] = prefvalue prf.close() def Ask(self, sentence, secondary=None): """ Pop-up a question message dialog """ self.questionbox = gtk.MessageDialog(self.window, 0, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL) self.questionbox.set_markup(sentence) if secondary != None: self.questionbox.format_secondary_markup(secondary) response = self.questionbox.run() self.questionbox.destroy() if response == -5: # The numeric code (-5) is returned from GTK when pressing the OK button return True else: return False def Say(self, sentence, secondary=None, msg_type=gtk.MESSAGE_INFO): """ Pop-up one button info message box """ self.messagebox = gtk.MessageDialog(None, gtk.DIALOG_MODAL, msg_type, gtk.BUTTONS_OK) self.messagebox.set_markup(sentence) if secondary != None: self.messagebox.format_secondary_markup(secondary) self.messagebox.run() self.messagebox.destroy() def LaunchBrowser(self, widget, link): """ Launch a browser for the specified link """ webbrowser.open(link) def OnIpodConnect(self, ipod_hal, ipod_udi): """ Do stuff when an iPod has been connected """ self.SetSensitive(('eject1', 'ejectbutton'), True) mountpoint = ipod_hal.get_ipod_mount_point(ipod_udi) self.ipod_mountpoint = mountpoint standard_loc = os.path.join(mountpoint, 'Photos', 'Photo Database') if os.path.isfile(standard_loc): if self.prefs['ipod_askbeforeopen'] == False or self.Ask(_('<b>iPod</b> and existing <b>Photo Database</b> found.'), _('Do you want to load the Photo Database found in the iPod attached to <b><i>%s</i></b>?') % mountpoint): self.DBOpen(standard_loc) else: if self.Ask(_('<b>iPod</b> found, attached to <b><i>%s</i></b>.') % mountpoint, _('But Photo Database <b>not found</b>. Do you want to create a new one?')): self.DBOpen(standard_loc) def OnIpodDisconnect(self, ipod_hal, ipod_udi): """ Do stuff when the iPod has been disconnected """ self.SetSensitive(('eject1', 'ejectbutton'), False) if self.DB != None: if self.win.get_widget('save1').get_property('sensitive'): unsaved_changes = _('All the unsaved changes will be lost.') else: unsaved_changes = _('There are not unsaved changes.') if self.Ask(_('<b>iPod</b> disconnected. Do you want to <b>exit</b> now?'), unsaved_changes): gtk.main_quit() else: self.Say(_('Please <b>reconnect the iPod</b> and <b>without loading again the Photo Database</b>'), _('Not reconnecting means that you could not save your changes, resulting in a crash. And re-opening would abort all changes'), gtk.MESSAGE_WARNING) if __name__ == '__main__': # FIXME: add exceptions handler! gpixpod = GPixPod() gtk.main()