#!/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&currency_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()
 
code/gpixpod-py.txt · Last modified: 2006/04/15 12:43
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki