diff options
author | Julien Valroff <julien@kirya.net> | 2011-04-16 16:39:15 +0200 |
---|---|---|
committer | Julien Valroff <julien@kirya.net> | 2011-04-16 16:39:15 +0200 |
commit | 75b28642dd41fb4a7925b42cb24274de90a4f52c (patch) | |
tree | ee6d94212855a1fdaf649e0c6d509951164edc96 /rapid | |
parent | 5168fdb07d6dc2b77f0ef9c7502940ce4a02e9aa (diff) |
Imported Upstream version 0.4.0~beta1upstream/0.4.0_beta1
Diffstat (limited to 'rapid')
50 files changed, 12459 insertions, 10681 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index 292d4b8..c9437d5 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,39 @@ +Version 0.4.0 beta 1 +-------------------- + +2011-04-10 + +Features added since alpha 4: + +* Job Code functionality, mimicking that found in version 0.2.3. +* Eject device button for each unmountable device in main window. +* When not all files have been downloaded from a device, the number remaining + is displayed in the device's progress bar +* Overall download progress is displayed in progress bar at bottom of window +* Time remaining and download speed are displayed in the status bar +* System notification messages +* Automation features: + * Automatically start a download at program startup or when a device + is inserted. When this is enabled, to optimize performance instead of + thumbnails being generated before the files are downloaded, they are + generated during the download. + * Eject a device when all files have been downloaded from it. + * Exit when all files have been downloaded. + +The automation feature to delete downloaded files from a device will be added +only when the non-alpha/beta of version 0.4.0 is released. + +The major feature currently not implemented is backups. + +Note: if videos are downloaded, the device may not be able to be unmounted +until Rapid Photo Downloader is exited. See bug #744012 for details. + +Bug fix: adjust vertical pane position when additional devices are inserted +Bug fix: display file and subfolder naming warnings in error log + +Updated Czech, French and Russian translations. + + Version 0.3.6 ------------- @@ -10,12 +46,102 @@ It also contains a minor packaging change so it can be installed in Ubuntu 11.04. +Version 0.4.0 alpha 4 +--------------------- + +2011-04-04 + +Fixed bug #750808: errorlog.ui not included in setup.py. + + +Version 0.4.0 alpha 3 +--------------------- + +2011-04-04 + +Features added since alpha 2: + +* Error log window to display download warnings and errors. +* Synchronize RAW + JPEG Sequence values. + +Fixed bug #739021: unable to set subfolder and file rename preferences on +alpha and beta Linux distributions such as Ubuntu 11.04 or Fedora 15. + +Updated Brazilian, Dutch, French, German and Spanish translations. + +Version 0.4.0 alpha 2 +--------------------- + +2011-03-31 + +Features added since alpha 1: + +* Sample file names and subfolders are now displayed in the preferences dialog + window. +* The option to add a unique identifier to a filename if a file with the same + name already exists + +Other changes: + +* Updated INSTALL file to match new package requirements. + +* Added program icon to main window. + +* Bug fix: leave file preview mode when download devices are changed in the + preferences. + +* Bug fix: don't crash on startup when trying to display free space and photo or + video download folders do not exist. + + +Version 0.4.0 alpha 1 +--------------------- + +2011-03-24 + +Rapid Photo Downloader is much faster and sports a new user interface. It is +about 50 times faster in tasks like scanning photos and videos before the +download. It also performs the actual downloads quicker. It will use +multiple CPU cores if they are available. + +Rapid Photo Downloader now requires version 0.3.0 or newer of pyexiv2. It also +requries Python Imaging (PIL) to run. It will only run on recent Linux +distributions such as Ubuntu 10.04 or newer. It has been tested on Ubuntu 10.04, +10.10 and 11.04, as well as Fedora 14. (There is currently an unusual bug +adjusting some preferences when running Ubuntu 11.04. See bug #739021). + +This is an alpha release because it is missing features that are present in +version 0.3.5. Missing features include: + +* System Notifications of download completion +* Job Codes +* Backups as you download +* Automation features, e.g. automatically start download at startup +* Error log window (currently you must check the command line for error output) +* Time remaining status messages +* Synchronize RAW + JPEG Sequence Numbers +* Add unique identifier to a filename if a file with the same name already + exists +* Sample file names and subfolders are not displayed in the preferences window + +These missing features will be added in subsequent alpha and beta releases. + +Kaa-metadata is no longer required to download videos. However, if you +want to use Frames Per Second or Codec metadata information in subfolder or +video file names, you must ensure it is installed. This is no longer checked at +program startup. + +Thanks go to Robert Park for refreshing the translations code. + +Added Romanian translation. + + Version 0.3.5 ------------- 2011-03-23 -The primary purpose of this release is update translations and fix bug #714039, +The primrary purpose of this release is update translations and fix bug #714039, where under certain circumstances the program could crash while downloading files. diff --git a/rapid/INSTALL b/rapid/INSTALL index 8270a20..8cde587 100644 --- a/rapid/INSTALL +++ b/rapid/INSTALL @@ -1,24 +1,15 @@ -Rapid Photo Downloader depends on the following software: - -- GNOME 2.18 or higher -- GTK+ 2.10 or higher -- Python 2.5 or 2.6 -- pygtk 2.12 or higher -- python-gconf 2.18 or higher -- python-glade2 2.12 or higher -- gnome-python 2.12 or higher -- libexiv2 0.15 or higher -- pyexiv2 0.1.1 or higher +Rapid Photo Downloader requires the following software: -To run Rapid Photo Downloader you will need all the software mentioned above. - -If you want to see dropshadows around thumbnail images, install python-imaging. -This is optional but recommended. +* Python 2.6 or 2.7 +* Pyexiv2 0.3.0 or higher +* python-gnome2 2.28 or higher +* python-gtk2 2.17 or higher +* python-gconf 2.28 or higher +* python-notify 0.1.1 or higher +* python-imaging 1.1.7 or higher +* librsvg2-common 2.26 or higher -A strongly recommended package is exiv2 (not just the library libexiv2), so -Rapid Photo Downloader can determine if it can download additional types of RAW -files (some early versions of exiv2 and pyexiv2 segfault on certain RAW file -types). +To run Rapid Photo Downloader you will need all the software mentioned above. If you want to download videos, you should install: @@ -29,7 +20,7 @@ If you want to download videos, you should install: hachoir metadata is required to download videos. kaa metadata is used to extract additional metadata from videos. ffmpegthumbnailer is used only to display thumbnail images before the download occurs. This is a useful feature, -and if you can install it, it is recommended. +and if you can install it, it is strongly recommended. hachoir metadata, kaa metadata and ffmpegthumbnailer are optional. The program will run without them. diff --git a/rapid/common.py b/rapid/common.py deleted file mode 100644 index a55d835..0000000 --- a/rapid/common.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007-09 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import os -import sys -import gc -import distutils.version -import gtk.gdk as gdk -import gtk -try: - import gio -except: - pass - -import config - -import locale -import gettext - -class Configi18n: - """ Setup translation - - Adapated from code example of Mark Mruss http://www.learningpython.com. - Unlike his example, this code uses a local locale directory only if the environment - variable LOCALEDIR has been set to some or other value. - """ - - # Do not put this code block in __init__, because it needs to be run only once - - # if the evironment value 'LOCAELDIR' is set, then use this as the source of translation data - # otherwise, rely on the system-wide data - locale_path = os.environ.get('LOCALEDIR', None) - - # Init the list of languages to support - langs = [] - #Check the default locale - lc, encoding = locale.getdefaultlocale() - if (lc): - #If we have a default, it's the first in the list - langs = [lc] - # Now let's get all of the supported languages on the system - language = os.environ.get('LANGUAGE', None) - if (language): - # langage comes back something like en_CA:en_US:en_GB:en - langs += language.split(":") - - # add on to the back of the list the translations that we know that we have, our defaults - langs += ["en_US"] - - # Now langs is a list of all of the languages that we are going - # to try to use. First we check the default, then what the system - # told us, and finally the 'known' list - - gettext.bindtextdomain(config.APP_NAME, locale_path) - gettext.textdomain(config.APP_NAME) - # Get the language to use - lang = gettext.translation(config.APP_NAME, locale_path, languages=langs, fallback = True) - # Install the language, map _() (which we marked our - # strings to translate with) to self.lang.gettext() which will - # translate them. - _ = lang.gettext - - -def pythonifyVersion(v): - """ makes version number a version number in distutils sense""" - return distutils.version.StrictVersion(v.replace( '~','')) - -def getFullProgramName(): - """ return the full name of the process running """ - return os.path.basename(sys.argv[0]) - -def getProgramName(): - """ return the name of the process running, removing the .py extension if it exists """ - programName = getFullProgramName() - if programName.find('.py') > 0: - programName = programName[:programName.find('.py')] - return programName - -def splitDirectories(directories): - """ split directories specified in string into a list """ - if directories.find(',') > 0: - d = directories.split(',') - else: - d = directories.split() - directories = [] - for i in d: - directories.append(i.strip()) - return directories - - - -def getFullPath(path): - """ make path relative to home directory if not an absolute path """ - if os.path.isabs(path): - return path - else: - return os.path.join(os.path.expanduser('~'), path) - - -def escape(s): - """ - Replace special characters by SGML entities. - """ - entities = ("&&", "<<", ">>") - for e in entities: - s = s.replace(e[0], e[1:]) - return s - -def formatSizeForUser(bytes, zeroString="", withDecimals=True, kbOnly=False): - """Format an int containing the number of bytes into a string suitable for - printing out to the user. zeroString is the string to use if bytes == 0. - source: https://develop.participatoryculture.org/trac/democracy/browser/trunk/tv/portable/util.py?rev=3993 - - """ - if bytes > (1 << 30) and not kbOnly: - value = (bytes / (1024.0 * 1024.0 * 1024.0)) - if withDecimals: - format = "%1.1fGB" - else: - format = "%dGB" - elif bytes > (1 << 20) and not kbOnly: - value = (bytes / (1024.0 * 1024.0)) - if withDecimals: - format = "%1.1fMB" - else: - format = "%dMB" - elif bytes > (1 << 10): - value = (bytes / 1024.0) - if withDecimals: - format = "%1.1fKB" - else: - format = "%dKB" - elif bytes > 1: - value = bytes - if withDecimals: - format = "%1.1fB" - else: - format = "%dB" - else: - return zeroString - return format % value - -def scale2pixbuf(width_max, height_max, pixbuf, return_size=False): - """ - Scale to width_max and height_max. - Keep aspect ratio. - Code adapted from gthumpy, by guettli - """ - - width_orig = float(pixbuf.get_width()) - height_orig = float(pixbuf.get_height()) - if (width_orig / width_max) > (height_orig / height_max): - height = int((height_orig / width_orig) * width_max) - width = width_max - else: - width = int((width_orig / height_orig) * height_max) - height=height_max - - pixbuf = pixbuf.scale_simple(width, height, gdk.INTERP_BILINEAR) - gc.collect() # Tell Python to clean up the memory - if return_size: - return pixbuf, width_orig, height_orig - return pixbuf - -def get_icon_pixbuf(using_gio, icon, size, fallback='gtk-harddisk'): - """ returns icon for the volume, or None if not available""" - - icontheme = gtk.icon_theme_get_default() - - if using_gio: - f = None - if isinstance(icon, gio.ThemedIcon): - try: - # on some user's systems, themes do not have icons associated with them - iconinfo = icontheme.choose_icon(icon.get_names(), size, gtk.ICON_LOOKUP_USE_BUILTIN) - f = iconinfo.get_filename() - v = gtk.gdk.pixbuf_new_from_file_at_size(f, size, size) - except: - f = None - if not f: - v = icontheme.load_icon(fallback, size, gtk.ICON_LOOKUP_USE_BUILTIN) - else: - v = icontheme.load_icon(icon, size, gtk.ICON_LOOKUP_USE_BUILTIN) - return v - -def register_iconsets(icon_info): - """ - Register icons in the icon set if they're not already used - - From http://faq.pygtk.org/index.py?req=show&file=faq08.012.htp - """ - - iconfactory = gtk.IconFactory() - stock_ids = gtk.stock_list_ids() - for stock_id, file in icon_info: - # only load image files when our stock_id is not present - if stock_id not in stock_ids: - pixbuf = gtk.gdk.pixbuf_new_from_file(file) - iconset = gtk.IconSet(pixbuf) - iconfactory.add(stock_id, iconset) - iconfactory.add_default() - - - - -if __name__ == '__main__': - i = Configi18n() - _ = i._ - print _("hello world") diff --git a/rapid/config.py b/rapid/config.py index e798057..da2f5c0 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -1,5 +1,5 @@ # -*- coding: latin1 -*- -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch <damonlynch@gmail.com> +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch <damonlynch@gmail.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 @@ -15,10 +15,9 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -version = '0.3.6' +version = '0.4.0~b1' GCONF_KEY="/apps/rapid-photo-downloader" -GLADE_FILE = "glade3/rapid.glade" DBUS_NAME = "net.damonlynch.RapidPhotoDownloader" @@ -38,18 +37,10 @@ DEFAULT_VIDEO_BACKUP_LOCATION = 'Videos' DEFAULT_VIDEO_LOCATIONS = ['Videos'] -MAX_NO_READERS = 20 - CRITICAL_ERROR = 1 SERIOUS_ERROR = 2 WARNING = 3 -MAX_LENGTH_DEVICE_NAME = 15 - -MIN_THUMBNAIL_SIZE = 80 -max_thumbnail_size = 320 # will be overridden when the screen is tiny -THUMBNAIL_INCREMENT = 50 - STATUS_DOWNLOAD_PENDING = 0 # going to try to download it STATUS_DOWNLOADED = 1 # downloaded successfully STATUS_DOWNLOADED_WITH_WARNING = 2 # downloaded ok but there was a warning @@ -60,4 +51,7 @@ STATUS_DOWNLOAD_FAILED = 6 # tried to download but failed STATUS_WARNING = 7 # warning (shown in pre-download preview) STATUS_CANNOT_DOWNLOAD = 8 # cannot be downloaded -TINY_SCREEN_HEIGHT = 650 +DEFAULT_WINDOW_WIDTH = 670 +DEFAULT_WINDOW_HEIGHT = 650 + + diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py new file mode 100644 index 0000000..08daafe --- /dev/null +++ b/rapid/copyfiles.py @@ -0,0 +1,200 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import multiprocessing +import tempfile +import os + +import gio + +import logging +logger = multiprocessing.get_logger() + +import rpdmultiprocessing as rpdmp +import rpdfile +import problemnotification as pn +import config +import thumbnail as tn + + +from gettext import gettext as _ + + +class CopyFiles(multiprocessing.Process): + def __init__(self, photo_download_folder, video_download_folder, files, + generate_thumbnails, scan_pid, + batch_size_MB, results_pipe, terminate_queue, + run_event): + multiprocessing.Process.__init__(self) + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.batch_size_bytes = batch_size_MB * 1048576 # * 1024 * 1024 + self.photo_download_folder = photo_download_folder + self.video_download_folder = video_download_folder + self.files = files + self.generate_thumbnails = generate_thumbnails + self.scan_pid = scan_pid + self.no_files= len(self.files) + self.run_event = run_event + + def check_termination_request(self): + """ + Check to see this process has not been requested to immediately terminate + """ + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + logger.info("Terminating file copying") + return True + return False + + + def update_progress(self, amount_downloaded, total): + # first check if process is being terminated + if not self.terminate_queue.empty(): + # it is - cancel the current copy + self.cancel_copy.cancel() + else: + chunk_downloaded = amount_downloaded - self.bytes_downloaded + if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total): + self.bytes_downloaded = amount_downloaded + if amount_downloaded == total: + # this function is called a couple of times when total is reached + chunk_downloaded = 0 + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded, chunk_downloaded)))) + if amount_downloaded == total: + self.bytes_downloaded = 0 + + def progress_callback(self, amount_downloaded, total): + self.update_progress(amount_downloaded, total) + + + def run(self): + """start the actual copying of files""" + + self.bytes_downloaded = 0 + self.total_downloaded = 0 + + self.cancel_copy = gio.Cancellable() + + self.create_temp_dirs() + + # Send the location of both temporary directories, so they can be + # removed once another process attempts to rename all the files in them + # and move them to generated subfolders + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_TEMP_DIRS, + (self.scan_pid, + self.photo_temp_dir, + self.video_temp_dir)))) + + if self.photo_temp_dir or self.video_temp_dir: + + if self.generate_thumbnails: + self.thumbnail_maker = tn.Thumbnail() + + for i in range(len(self.files)): + rpd_file = self.files[i] + + # pause if instructed by the caller + self.run_event.wait() + + if self.check_termination_request(): + return None + + source = gio.File(path=rpd_file.full_file_name) + temp_full_file_name = os.path.join( + self._get_dest_dir(rpd_file.file_type), + rpd_file.name) + rpd_file.temp_full_file_name = temp_full_file_name + dest = gio.File(path=temp_full_file_name) + + copy_succeeded = False + try: + source.copy(dest, self.progress_callback, cancellable=self.cancel_copy) + copy_succeeded = True + except gio.Error, inst: + rpd_file.add_problem(None, + pn.DOWNLOAD_COPYING_ERROR_W_NO, + {'filetype': rpd_file.title}) + rpd_file.add_extra_detail( + pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, + {'errorno': inst.code, 'strerror': inst.message}) + + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + + rpd_file.error_title = rpd_file.problem.get_title() + rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ + {'problem': rpd_file.problem.get_problems(), + 'file': rpd_file.full_file_name} + + logger.error("Failed to download file: %s", rpd_file.full_file_name) + logger.error(inst) + self.update_progress(rpd_file.size, rpd_file.size) + + # increment this amount regardless of whether the copy actually + # succeeded or not. It's neccessary to keep the user informed. + self.total_downloaded += rpd_file.size + + if copy_succeeded and self.generate_thumbnails: + thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( + temp_full_file_name, + rpd_file.file_type, + (160, 120), (100,100)) + self.results_pipe.send((rpdmp.CONN_PARTIAL, + (rpdmp.MSG_THUMB, (rpd_file.unique_id, + thumbnail_icon, thumbnail)))) + + if rpd_file.metadata is not None: + rpd_file.metadata = None + + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, + (copy_succeeded, rpd_file, i + 1, temp_full_file_name)))) + + + self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) + + + def _get_dest_dir(self, file_type): + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.photo_temp_dir + else: + return self.video_temp_dir + + def _create_temp_dir(self, folder): + try: + temp_dir = tempfile.mkdtemp(prefix="rpd-tmp-", dir=folder) + except OSError, (errno, strerror): + # FIXME: error reporting + logger.error("Failed to create temporary directory in %s: %s %s", + errono, + strerror, + folder) + temp_dir = None + + return temp_dir + + def create_temp_dirs(self): + self.photo_temp_dir = self.video_temp_dir = None + if self.photo_download_folder is not None: + self.photo_temp_dir = self._create_temp_dir(self.photo_download_folder) + if self.video_download_folder is not None: + self.video_temp_dir = self._create_temp_dir(self.photo_download_folder) + + + diff --git a/rapid/device.py b/rapid/device.py new file mode 100644 index 0000000..dcfdf94 --- /dev/null +++ b/rapid/device.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import gtk, gio + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import paths +import utilities + +from gettext import gettext as _ + +class Device: + def __init__(self, mount=None, path=None): + self.mount = mount + self.path = path + + def get_path(self): + if self.mount: + return self.mount.get_root().get_path() + else: + return self.path + + def get_name(self): + if self.mount: + return self.mount.get_name() + else: + return self.path + + def get_icon(self, size=16): + if self.mount: + icon = self.mount.get_icon() + else: + folder = gio.File(self.path) + file_info = folder.query_info(gio.FILE_ATTRIBUTE_STANDARD_ICON) + icon = file_info.get_icon() + + icontheme = gtk.icon_theme_get_default() + + icon_file = None + if isinstance(icon, gio.ThemedIcon): + try: + # on some user's systems, themes do not have icons associated with them + iconinfo = icontheme.choose_icon(icon.get_names(), size, gtk.ICON_LOOKUP_USE_BUILTIN) + icon_file = iconinfo.get_filename() + return gtk.gdk.pixbuf_new_from_file_at_size(icon_file, size, size) + except: + pass + + if not icon_file: + return icontheme.load_icon('folder', size, gtk.ICON_LOOKUP_USE_BUILTIN) + + +class UseDeviceDialog(gtk.Dialog): + """ + Simple dialog window that prompt's the user whether to use a certain + device or not + """ + def __init__(self, parent_window, device, post_choice_callback): + gtk.Dialog.__init__(self, _('Device Detected'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, + gtk.STOCK_YES, gtk.RESPONSE_OK)) + + self.post_choice_callback = post_choice_callback + + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt + prompt_label = gtk.Label(_('Should this device or partition be used to download photos or videos from?')) + prompt_label.set_line_wrap(True) + prompt_hbox = gtk.HBox() + prompt_hbox.pack_start(prompt_label, False, False, padding=6) + device_label = gtk.Label() + device_label.set_markup("<b>%s</b>" % device.get_name()) + device_hbox = gtk.HBox() + device_hbox.pack_start(device_label, False, False) + path_label = gtk.Label() + path_label.set_markup("<i>%s</i>" % device.get_path()) + path_hbox = gtk.HBox() + path_hbox.pack_start(path_label, False, False) + + icon = device.get_icon(size=36) + if icon: + image = gtk.Image() + image.set_from_pixbuf(icon) + + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt + self.always_checkbutton = gtk.CheckButton(_('_Remember this choice'), True) + + if icon: + device_hbox_icon = gtk.HBox(homogeneous=False, spacing=6) + device_hbox_icon.pack_start(image, False, False, padding = 6) + device_vbox = gtk.VBox(homogeneous=True, spacing=6) + device_vbox.pack_start(device_hbox, False, False) + device_vbox.pack_start(path_hbox, False, False) + device_hbox_icon.pack_start(device_vbox, False, False) + self.vbox.pack_start(device_hbox_icon, padding = 6) + else: + self.vbox.pack_start(device_hbox, padding=6) + self.vbox.pack_start(path_hbox, padding = 6) + + self.vbox.pack_start(prompt_hbox, padding=6) + self.vbox.pack_start(self.always_checkbutton, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + self.set_default_response(gtk.RESPONSE_OK) + + + self.set_transient_for(parent_window) + self.show_all() + self.device = device + + self.connect('response', self.on_response) + + def on_response(self, device_dialog, response): + user_selected = False + permanent_choice = self.always_checkbutton.get_active() + if response == gtk.RESPONSE_OK: + user_selected = True + logger.info("%s selected for downloading from", self.device.get_name()) + if permanent_choice: + logger.info("This device or partition will always be used to download from") + else: + logger.info("%s rejected as a download device", self.device.get_name()) + if permanent_choice: + logger.info("This device or partition will never be used to download from") + + self.post_choice_callback(self, user_selected, permanent_choice, + self.device) + + +def is_DCIM_device(path): + """ Returns true if directory specifies media with photos on it""" + + test_path = os.path.join(path, "DCIM") + return utilities.is_directory(test_path) + +def is_backup_media(path, identifiers, writeable=True): + """ Test to see if path is used as a backup medium for storing photos or videos + + Identifiers is expected to be a list of folder names to check to see + if the path is a backup path. Only one of them needs to be present + for the path to be considered a backup medium. + + If writeable is True, the directory must be writeable by the user """ + suitable = False + + for identifier in identifiers: + if os.path.isdir(os.path.join(path, identifier)): + if writeable: + suitable = os.access(os.path.join(path, identifier), os.W_OK) + else: + suitable = True + return suitable + diff --git a/rapid/downloadtracker.py b/rapid/downloadtracker.py new file mode 100644 index 0000000..309da71 --- /dev/null +++ b/rapid/downloadtracker.py @@ -0,0 +1,275 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import time +from rpdfile import FILE_TYPE_PHOTO, FILE_TYPE_VIDEO +from config import STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOADED_WITH_WARNING + +from gettext import gettext as _ + +class DownloadTracker: + """ + Track file downloads - their size, number, and any problems + """ + def __init__(self): + self.size_of_download_in_bytes_by_scan_pid = dict() + self.total_bytes_copied_in_bytes_by_scan_pid = dict() + self.no_files_in_download_by_scan_pid = dict() + self.file_types_present_by_scan_pid = dict() + # 'Download count' tracks the index of the file being downloaded + # into the list of files that need to be downloaded -- much like + # a counter in a for loop, e.g. 'for i in list', where i is the counter + self.download_count_for_file_by_unique_id = dict() + self.download_count_by_scan_pid = dict() + self.rename_chunk = dict() + self.files_downloaded = dict() + self.photos_downloaded = dict() + self.videos_downloaded = dict() + self.photo_failures = dict() + self.video_failures = dict() + self.warnings = dict() + self.total_photos_downloaded = 0 + self.total_photo_failures = 0 + self.total_videos_downloaded = 0 + self.total_video_failures = 0 + self.total_warnings = 0 + self.total_bytes_to_download = 0 + + def init_stats(self, scan_pid, bytes, no_files): + self.no_files_in_download_by_scan_pid[scan_pid] = no_files + self.rename_chunk[scan_pid] = bytes / 10 / no_files + self.size_of_download_in_bytes_by_scan_pid[scan_pid] = bytes + self.rename_chunk[scan_pid] * no_files + self.total_bytes_to_download += self.size_of_download_in_bytes_by_scan_pid[scan_pid] + self.files_downloaded[scan_pid] = 0 + self.photos_downloaded[scan_pid] = 0 + self.videos_downloaded[scan_pid] = 0 + self.photo_failures[scan_pid] = 0 + self.video_failures[scan_pid] = 0 + self.warnings[scan_pid] = 0 + + def get_no_files_in_download(self, scan_pid): + return self.no_files_in_download_by_scan_pid[scan_pid] + + + def get_no_files_downloaded(self, scan_pid, file_type): + if file_type == FILE_TYPE_PHOTO: + return self.photos_downloaded.get(scan_pid, 0) + else: + return self.videos_downloaded.get(scan_pid, 0) + + def get_no_files_failed(self, scan_pid, file_type): + if file_type == FILE_TYPE_PHOTO: + return self.photo_failures.get(scan_pid, 0) + else: + return self.video_failures.get(scan_pid, 0) + + def get_no_warnings(self, scan_pid): + return self.warnings.get(scan_pid, 0) + + def file_downloaded_increment(self, scan_pid, file_type, status): + self.files_downloaded[scan_pid] += 1 + + if status <> STATUS_DOWNLOAD_FAILED: + if file_type == FILE_TYPE_PHOTO: + self.photos_downloaded[scan_pid] += 1 + self.total_photos_downloaded += 1 + else: + self.videos_downloaded[scan_pid] += 1 + self.total_videos_downloaded += 1 + + if status == STATUS_DOWNLOADED_WITH_WARNING: + self.warnings[scan_pid] += 1 + self.total_warnings += 1 + else: + if file_type == FILE_TYPE_PHOTO: + self.photo_failures[scan_pid] += 1 + self.total_photo_failures += 1 + else: + self.video_failures[scan_pid] += 1 + self.total_video_failures += 1 + + + def get_percent_complete(self, scan_pid): + """ + Returns a float representing how much of the download + has been completed + """ + + # three components: copy (download), rename, and backup + percent_complete = ((float( + self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid]) + + self.rename_chunk[scan_pid] * self.files_downloaded[scan_pid]) + / self.size_of_download_in_bytes_by_scan_pid[scan_pid]) * 100 + return percent_complete + + def get_overall_percent_complete(self): + total = 0 + for scan_pid in self.total_bytes_copied_in_bytes_by_scan_pid: + total += (self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid] + + (self.rename_chunk[scan_pid] * + self.files_downloaded[scan_pid])) + + percent_complete = float(total) / self.total_bytes_to_download + return percent_complete + + def set_total_bytes_copied(self, scan_pid, total_bytes): + self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid] = total_bytes + + def set_download_count_for_file(self, unique_id, download_count): + self.download_count_for_file_by_unique_id[unique_id] = download_count + + def get_download_count_for_file(self, unique_id): + return self.download_count_for_file_by_unique_id[unique_id] + + def set_download_count(self, scan_pid, download_count): + self.download_count_by_scan_pid[scan_pid] = download_count + + def get_file_types_present(self, scan_pid): + return self.file_types_present_by_scan_pid[scan_pid] + + def set_file_types_present(self, scan_pid, file_types_present): + self.file_types_present_by_scan_pid[scan_pid] = file_types_present + + def no_errors_or_warnings(self): + """ + Return True if there were no errors or warnings in the download + else return False + """ + return (self.total_warnings == 0 and + self.photo_failures == 0 and + self.video_failures == 0) + + def purge(self, scan_pid): + del self.no_files_in_download_by_scan_pid[scan_pid] + del self.size_of_download_in_bytes_by_scan_pid[scan_pid] + del self.photos_downloaded[scan_pid] + del self.videos_downloaded[scan_pid] + del self.files_downloaded[scan_pid] + del self.photo_failures[scan_pid] + del self.video_failures[scan_pid] + del self.warnings[scan_pid] + + def purge_all(self): + self.__init__() + + + +class TimeCheck: + """ + Record times downloads commmence and pause - used in calculating time + remaining. + + Also tracks and reports download speed. + + Note: This is completely independent of the file / subfolder naming + preference "download start time" + """ + + def __init__(self): + # set the number of seconds gap with which to measure download time remaing + self.download_time_gap = 3 + + self.reset() + + def reset(self): + self.mark_set = False + self.total_downloaded_so_far = 0 + self.total_download_size = 0 + self.size_mark = 0 + + def increment(self, bytes_downloaded): + self.total_downloaded_so_far += bytes_downloaded + + def set_download_mark(self): + if not self.mark_set: + self.mark_set = True + + self.time_mark = time.time() + + def pause(self): + self.mark_set = False + + def check_for_update(self): + now = time.time() + update = now > (self.download_time_gap + self.time_mark) + + if update: + amt_time = now - self.time_mark + self.time_mark = now + amt_downloaded = self.total_downloaded_so_far - self.size_mark + self.size_mark = self.total_downloaded_so_far + download_speed = "%1.1f" % (amt_downloaded / 1048576 / amt_time) +_("MB/s") + else: + download_speed = None + + return (update, download_speed) + +class TimeForDownload: + # used to store variables, see below + pass + +class TimeRemaining: + """ + Calculate how much time is remaining to finish a download + """ + gap = 3 + def __init__(self): + self.clear() + + def set(self, scan_pid, size): + t = TimeForDownload() + t.time_remaining = None + t.size = size + t.downloaded = 0 + t.size_mark = 0 + t.time_mark = time.time() + self.times[scan_pid] = t + + def update(self, scan_pid, total_size): + if scan_pid in self.times: + self.times[scan_pid].downloaded = total_size + now = time.time() + tm = self.times[scan_pid].time_mark + amt_time = now - tm + if amt_time > self.gap: + self.times[scan_pid].time_mark = now + amt_downloaded = self.times[scan_pid].downloaded - self.times[scan_pid].size_mark + self.times[scan_pid].size_mark = self.times[scan_pid].downloaded + timefraction = amt_downloaded / float(amt_time) + amt_to_download = float(self.times[scan_pid].size) - self.times[scan_pid].downloaded + if timefraction: + self.times[scan_pid].time_remaining = amt_to_download / timefraction + + def _time_estimates(self): + for t in self.times: + yield self.times[t].time_remaining + + def time_remaining(self): + return max(self._time_estimates()) + + def set_time_mark(self, scan_pid): + if scan_pid in self.times: + self.times[scan_pid].time_mark = time.time() + + def clear(self): + self.times = {} + + def remove(self, scan_pid): + if scan_pid in self.times: + del self.times[scan_pid] diff --git a/rapid/dropshadow.py b/rapid/dropshadow.py deleted file mode 100755 index 68b5398..0000000 --- a/rapid/dropshadow.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/python - - -import StringIO -import gtk -from PIL import Image, ImageFilter - -def image_to_pixbuf(image): - # this one handles transparency, unlike the default example in the pygtk FAQ - # this is also from the pygtk FAQ - IS_RGBA = image.mode=='RGBA' - return gtk.gdk.pixbuf_new_from_data( - image.tostring(), # data - gtk.gdk.COLORSPACE_RGB, # color mode - IS_RGBA, # has alpha - 8, # bits - image.size[0], # width - image.size[1], # height - (IS_RGBA and 4 or 3) * image.size[0] # rowstride - ) - - -def image_to_pixbuf_no_transparency(image): - fd = StringIO.StringIO() - image.save(fd, "ppm") - contents = fd.getvalue() - fd.close() - loader = gtk.gdk.PixbufLoader("pnm") - loader.write(contents, len(contents)) - pixbuf = loader.get_pixbuf() - loader.close() - return pixbuf - -def pixbuf_to_image(pb): - assert(pb.get_colorspace() == gtk.gdk.COLORSPACE_RGB) - dimensions = pb.get_width(), pb.get_height() - stride = pb.get_rowstride() - pixels = pb.get_pixels() - - mode = pb.get_has_alpha() and "RGBA" or "RGB" - image = Image.frombuffer(mode, dimensions, pixels, - "raw", mode, stride, 1) - - if mode == "RGB": - # convert to having an alpha value, so that the image can - # act as a mask in the drop shadow paste - image = image.convert("RGBA") - - return image - - -class DropShadow(): - """ - Adds a gaussian blur drop shadow to a PIL image. - - Caches backgrounds of particular sizes for improved performance. - - Backgrounds can be made transparent. - - Modification of code from Kevin Schluff and Matimus - License: Python license - See: - http://code.activestate.com/recipes/474116/ (r2) - http://bytes.com/topic/python/answers/606952-pil-paste-image-top-other-dropshadow - - """ - - def __init__(self, offset=(5,5), background_color=0xffffff, shadow = (0x44, 0x44, 0x44, 0xff), - border=8, iterations=3, trim_border=False): - """ - offset - Offset of the shadow from the image as an (x,y) tuple. Can be - positive or negative. - background_color - Background colour behind the image. - shadow - Shadow colour (darkness). - border - Width of the border around the image. This must be wide - enough to account for the blurring of the shadow. - trim_border - If true, the border will only be created on the - sides it needs to be (i.e. only on two sides) - iterations - Number of times to apply the filter. More iterations - produce a more blurred shadow, but increase processing time. - - To make backgrounds transparent, ensure the alpha value of the shadow color is the - same as the background color, e.g. if background_color is 0xffffff, shadow's alpha should be 0xff - - The image must be in RGBA format. - """ - self.backgrounds = {} - self.offset = offset - self.background_color = background_color - self.shadow = shadow - self.border = border - self.trim_border = trim_border - self.iterations = iterations - - if self.offset[0] < 0 or not self.trim_border: - self.left_spacing = self.border - else: - self.left_spacing = 0 - - if self.offset[1] < 0 or not self.trim_border: - self.top_spacing = self.border - else: - self.top_spacing = 0 - - def dropShadow(self, image): - """ - image - The image to overlay on top of the shadow. - """ - dimensions = (image.size[0], image.size[1]) - if not dimensions in self.backgrounds: - - # Create the backdrop image -- a box in the background colour with a - # shadow on it. - - if self.trim_border: - totalWidth = image.size[0] + abs(self.offset[0]) + self.border - totalHeight = image.size[1] + abs(self.offset[1]) + self.border - else: - totalWidth = image.size[0] + abs(self.offset[0]) + 2 * self.border - totalHeight = image.size[1] + abs(self.offset[1]) + 2 * self.border - - back = Image.new("RGBA", (totalWidth, totalHeight), self.background_color) - - # Place the shadow, taking into account the offset from the image - if self.offset[0] > 0 and self.trim_border: - shadowLeft = max(self.offset[0], 0) - else: - shadowLeft = self.border + max(self.offset[0], 0) - if self.offset[1] > 0 and self.trim_border: - shadowTop = max(self.offset[1], 0) - else: - shadowTop = self.border + max(self.offset[1], 0) - - back.paste(self.shadow, [shadowLeft, shadowTop, shadowLeft + image.size[0], - shadowTop + image.size[1]] ) - - # Apply the filter to blur the edges of the shadow. Since a small kernel - # is used, the filter must be applied repeatedly to get a decent blur. - n = 0 - while n < self.iterations: - back = back.filter(ImageFilter.BLUR) - n += 1 - - self.backgrounds[dimensions] = back - - # Paste the input image onto the shadow backdrop - imageLeft = self.left_spacing - min(self.offset[0], 0) - imageTop = self.top_spacing - min(self.offset[1], 0) - - back = self.backgrounds[dimensions].copy() - back.paste(image, (imageLeft, imageTop), image) - - return back - - - -if __name__ == "__main__": - import sys - import os - import common - - - # create another file with a drop shadow - f = sys.argv[1] - - image = Image.open(f) - image.thumbnail((60,36), Image.ANTIALIAS) - image2 = image.copy() - - path, name = os.path.split(f) - name, ext = os.path.splitext(name) - - #image = dropShadow(image, shadow = (0x44, 0x44, 0x44, 0xff)) - dropShadow = DropShadow(offset=(3,3), shadow = (0x34, 0x34, 0x34, 0xff), border=6) - image = dropShadow.dropShadow(image) - image2 = dropShadow.dropShadow(image2) - - nf = os.path.join(path, "%s_small_shadow%s" % (name, ext)) - nf2 = os.path.join(path, "%s_small_shadow2%s" % (name, ext)) - image.save(nf) - image2.save(nf2) - print "wrote %s , %s" % (nf, nf2) - diff --git a/rapid/errorlog.py b/rapid/errorlog.py new file mode 100644 index 0000000..0334465 --- /dev/null +++ b/rapid/errorlog.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import gtk + +import pango +import config +import paths + +class ErrorLog(): + """ + Displays a log of errors, warnings or other information to the user + """ + + def __init__(self, rapidapp): + """ + Initialize values for log dialog, but do not display. + """ + + self.builder = gtk.Builder() + self.builder.add_from_file(paths.share_dir("glade3/errorlog.ui")) + self.builder.connect_signals(self) + self.widget = self.builder.get_object("errorlog") + self.log_textview = self.builder.get_object("log_textview") + self.log_scrolledwindow = self.builder.get_object("log_scrolledwindow") + + self.widget.connect("delete-event", self.hide_window) + + self.rapidapp = rapidapp + #~ self.log_textview.set_cursor_visible(False) + self.textbuffer = self.log_textview.get_buffer() + + self.error_tag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD, foreground="red") + self.warning_tag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) + self.extra_detail_tag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) + + def add_message(self, severity, problem, details, extra_detail): + if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: + self.rapidapp.error_image.show() + elif severity == config.WARNING: + self.rapidapp.warning_image.show() + self.rapidapp.warning_vseparator.show() + + iter = self.textbuffer.get_end_iter() + if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: + self.textbuffer.insert_with_tags(iter, problem +"\n", self.error_tag) + else: + self.textbuffer.insert_with_tags(iter, problem +"\n", self.warning_tag) + if details: + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert(iter, details + "\n") + if extra_detail: + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert_with_tags(iter, extra_detail +"\n", self.extra_detail_tag) + + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert(iter, "\n") + + # move viewport to display the latest message + adjustment = self.log_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.upper) + + + def on_errorlog_response(self, dialog, arg): + if arg == gtk.RESPONSE_CLOSE: + pass + self.rapidapp.error_image.hide() + self.rapidapp.warning_image.hide() + self.rapidapp.warning_vseparator.hide() + self.rapidapp.prefs.show_log_dialog = False + self.widget.hide() + return True + + def hide_window(self, window, event): + window.hide() + return True diff --git a/rapid/filmstrip.py b/rapid/filmstrip.py index 275fd1d..fc751fd 100755 --- a/rapid/filmstrip.py +++ b/rapid/filmstrip.py @@ -18,7 +18,7 @@ ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Adds a filmstrip to the left and right of a file +Adds a filmstrip to the left and right of a pixbuf """ import gtk diff --git a/rapid/generatename.py b/rapid/generatename.py new file mode 100644 index 0000000..e904a70 --- /dev/null +++ b/rapid/generatename.py @@ -0,0 +1,480 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os, re, datetime, string, collections + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import problemnotification as pn + +from generatenameconfig import * + +from gettext import gettext as _ + + +def convert_date_for_strftime(datetime_user_choice): + try: + return DATE_TIME_CONVERT[LIST_DATE_TIME_L2.index(datetime_user_choice)] + except: + raise PrefValueInvalidError(datetime_user_choice) + + +class PhotoName: + """ + Generate the name of a photo. Used as a base class for generating names + of videos, as well as subfolder names for both file types + """ + + def __init__(self, pref_list): + self.pref_list = pref_list + + + # Some of the next values are overwritten in derived classes + self.strip_initial_period_from_extension = False + self.strip_forward_slash = True + self.L1_date_check = IMAGE_DATE #used in _get_date_component() + self.component = pn.FILENAME_COMPONENT #used in error reporting + + + def _get_values_from_pref_list(self): + for i in range(0, len(self.pref_list), 3): + yield (self.pref_list[i], self.pref_list[i+1], self.pref_list[i+2]) + + def _get_date_component(self): + """ + Returns portion of new file / subfolder name based on date time. + If the date is missing, will attempt to use the fallback date. + """ + + # step 1: get the correct value from metadata + if self.L1 == self.L1_date_check: + if self.L2 == SUBSECONDS: + d = self.rpd_file.metadata.sub_seconds() + if d == '00': + self.rpd_file.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L2)) + return '' + else: + return d + else: + d = self.rpd_file.metadata.date_time(missing=None) + + elif self.L1 == TODAY: + d = datetime.datetime.now() + elif self.L1 == YESTERDAY: + delta = datetime.timedelta(days = 1) + d = datetime.datetime.now() - delta + elif self.L1 == DOWNLOAD_TIME: + d = self.rpd_file.download_start_time + else: + raise("Date options invalid") + + # step 2: if have a value, try to convert it to string format + if d: + try: + return d.strftime(convert_date_for_strftime(self.L2)) + except: + logger.warning("Exif date time value appears invalid for file %s", self.rpd_file.full_file_name) + + # step 3: handle a missing value using file modification time + if self.rpd_file.modification_time: + try: + d = datetime.datetime.fromtimestamp(self.rpd_file.modification_time) + except: + self.rpd_file.add_problem(self.component, pn.INVALID_DATE_TIME, '') + logger.error("Both file modification time and metadata date & time are invalid for file %s", self.rpd_file.full_file_name) + return '' + else: + self.rpd_file.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) + return '' + + try: + return d.strftime(convert_date_for_strftime(self.L2)) + except: + self.rpd_file.add_problem(self.component, pn.INVALID_DATE_TIME, d) + logger.error("Both file modification time and metadata date & time are invalid for file %s", self.rpd_file.full_file_name) + return '' + + def _get_filename_component(self): + """ + Returns portion of new file / subfolder name based on the file name + """ + + name, extension = os.path.splitext(self.rpd_file.name) + + if self.L1 == NAME_EXTENSION: + filename = self.rpd_file.name + elif self.L1 == NAME: + filename = name + elif self.L1 == EXTENSION: + if extension: + if not self.strip_initial_period_from_extension: + # keep the period / dot of the extension, so the user does not + # need to manually specify it + filename = extension + else: + # having the period when this is used as a part of a subfolder name + # is a bad idea when it is at the start! + filename = extension[1:] + else: + self.rpd_file.add_problem(self.component, pn.MISSING_FILE_EXTENSION) + return "" + elif self.L1 == IMAGE_NUMBER or self.L1 == VIDEO_NUMBER: + n = re.search("(?P<image_number>[0-9]+$)", name) + if not n: + self.rpd_file.add_problem(self.component, pn.MISSING_IMAGE_NUMBER) + return '' + else: + image_number = n.group("image_number") + + if self.L2 == IMAGE_NUMBER_ALL: + filename = image_number + elif self.L2 == IMAGE_NUMBER_1: + filename = image_number[-1] + elif self.L2 == IMAGE_NUMBER_2: + filename = image_number[-2:] + elif self.L2 == IMAGE_NUMBER_3: + filename = image_number[-3:] + elif self.L2 == IMAGE_NUMBER_4: + filename = image_number[-4:] + else: + raise TypeError("Incorrect filename option") + + if self.L2 == UPPERCASE: + filename = filename.upper() + elif self.L2 == LOWERCASE: + filename = filename.lower() + + return filename + + def _get_metadata_component(self): + """ + Returns portion of new image / subfolder name based on the metadata + + Note: date time metadata found in _getDateComponent() + """ + + if self.L1 == APERTURE: + v = self.rpd_file.metadata.aperture() + elif self.L1 == ISO: + v = self.rpd_file.metadata.iso() + elif self.L1 == EXPOSURE_TIME: + v = self.rpd_file.metadata.exposure_time(alternativeFormat=True) + elif self.L1 == FOCAL_LENGTH: + v = self.rpd_file.metadata.focal_length() + elif self.L1 == CAMERA_MAKE: + v = self.rpd_file.metadata.camera_make() + elif self.L1 == CAMERA_MODEL: + v = self.rpd_file.metadata.camera_model() + elif self.L1 == SHORT_CAMERA_MODEL: + v = self.rpd_file.metadata.short_camera_model() + elif self.L1 == SHORT_CAMERA_MODEL_HYPHEN: + v = self.rpd_file.metadata.short_camera_model(includeCharacters = "\-") + elif self.L1 == SERIAL_NUMBER: + v = self.rpd_file.metadata.camera_serial() + elif self.L1 == SHUTTER_COUNT: + v = self.rpd_file.metadata.shutter_count() + if v: + v = int(v) + padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3 + formatter = '%0' + str(padding) + "i" + v = formatter % v + + elif self.L1 == OWNER_NAME: + v = self.rpd_file.metadata.owner_name() + else: + raise TypeError("Invalid metadata option specified") + if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, + SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME]: + if self.L2 == UPPERCASE: + v = v.upper() + elif self.L2 == LOWERCASE: + v = v.lower() + if not v: + self.rpd_file.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) + return v + + def _calculate_letter_sequence(self, sequence): + + def _letters(x): + """ + Adapted from algorithm at http://en.wikipedia.org/wiki/Hexavigesimal + """ + v = '' + while x > 25: + r = x % 26 + x= x / 26 - 1 + v = string.lowercase[r] + v + v = string.lowercase[x] + v + + return v + + + v = _letters(sequence) + if self.L2 == UPPERCASE: + v = v.upper() + + return v + + def _format_sequence_no(self, value, amountToPad): + padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1 + formatter = '%0' + str(padding) + "i" + return formatter % value + + def _get_downloads_today(self): + return self._format_sequence_no(self.rpd_file.sequences.get_downloads_today(), self.L2) + + def _get_session_sequence_no(self): + return self._format_sequence_no(self.rpd_file.sequences.get_session_sequence_no(), self.L2) + + def _get_stored_sequence_no(self): + return self._format_sequence_no(self.rpd_file.sequences.get_stored_sequence_no(), self.L2) + + def _get_sequence_letter(self): + return self._calculate_letter_sequence(self.rpd_file.sequences.get_sequence_letter()) + + def _get_sequences_component(self): + if self.L1 == DOWNLOAD_SEQ_NUMBER: + return self._get_downloads_today() + elif self.L1 == SESSION_SEQ_NUMBER: + return self._get_session_sequence_no() + elif self.L1 == STORED_SEQ_NUMBER: + return self._get_stored_sequence_no() + elif self.L1 == SEQUENCE_LETTER: + return self._get_sequence_letter() + + + #~ elif self.L1 == SUBFOLDER_SEQ_NUMBER: + #~ return self._getSubfolderSequenceNo() + + + + def _get_component(self): + try: + if self.L0 == DATE_TIME: + return self._get_date_component() + elif self.L0 == TEXT: + return self.L1 + elif self.L0 == FILENAME: + return self._get_filename_component() + elif self.L0 == METADATA: + return self._get_metadata_component() + elif self.L0 == SEQUENCES: + return self._get_sequences_component() + elif self.L0 == JOB_CODE: + return self.rpd_file.job_code + elif self.L0 == SEPARATOR: + return os.sep + except: + self.rpd_file.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0)) + return '' + + + def generate_name(self, rpd_file): + self.rpd_file = rpd_file + + name = '' + + for self.L0, self.L1, self.L2 in self._get_values_from_pref_list(): + v = self._get_component() + if v: + name += v + + if self.rpd_file.strip_characters: + for c in r'\:*?"<>|': + name = name.replace(c, '') + + if self.strip_forward_slash: + name = name.replace('/', '') + + name = name.strip() + + return name + + + + +class VideoName(PhotoName): + def __init__(self, pref_list): + PhotoName.__init__(self, pref_list) + self.L1_date_check = VIDEO_DATE #used in _get_date_component() + + def _get_metadata_component(self): + """ + Returns portion of video / subfolder name based on the metadata + + Note: date time metadata found in _getDateComponent() + """ + return get_video_metadata_component(self) + +class PhotoSubfolder(PhotoName): + """ + Generate subfolder names for photo files + """ + + def __init__(self, pref_list): + self.pref_list = pref_list + + self.strip_extraneous_white_space = re.compile(r'\s*%s\s*' % os.sep) + self.strip_initial_period_from_extension = True + self.strip_forward_slash = False + self.L1_date_check = IMAGE_DATE #used in _get_date_component() + self.component = pn.SUBFOLDER_COMPONENT #used in error reporting + + def generate_name(self, rpd_file): + + subfolders = PhotoName.generate_name(self, rpd_file) + + # subfolder value must never start with a separator, or else any + # os.path.join function call will fail to join a subfolder to its + # parent folder + if subfolders: + if subfolders[0] == os.sep: + subfolders = subfolders[1:] + + # remove any spaces before and after a directory name + if subfolders and self.rpd_file.strip_characters: + subfolders = self.strip_extraneous_white_space.sub(os.sep, subfolders) + + return subfolders + + + + +class VideoSubfolder(PhotoSubfolder): + """ + Generate subfolder names for video files + """ + + def __init__(self, pref_list): + PhotoSubfolder.__init__(self, pref_list) + self.L1_date_check = VIDEO_DATE #used in _get_date_component() + + + def _get_metadata_component(self): + """ + Returns portion of video / subfolder name based on the metadata + + Note: date time metadata found in _getDateComponent() + """ + return get_video_metadata_component(self) + +def get_video_metadata_component(video): + """ + Returns portion of video / subfolder name based on the metadata + + This is outside of a class definition because of the inheritence + hierarchy. + """ + + problem = None + if video.L1 == CODEC: + v = video.rpd_file.metadata.codec() + elif video.L1 == WIDTH: + v = video.rpd_file.metadata.width() + elif video.L1 == HEIGHT: + v = video.rpd_file.metadata.height() + elif video.L1 == FPS: + v = video.rpd_file.metadata.frames_per_second() + elif video.L1 == LENGTH: + v = video.rpd_file.metadata.length() + else: + raise TypeError("Invalid metadata option specified") + if video.L1 in [CODEC]: + if video.L2 == UPPERCASE: + v = v.upper() + elif video.L2 == LOWERCASE: + v = v.lower() + if not v: + video.rpd_file.add_problem(video.component, pn.MISSING_METADATA, _(video.L1)) + return v + +class Sequences: + """ + Holds sequence numbers and letters used in generating filenames. + """ + def __init__(self, downloads_today_tracker, stored_sequence_no): + self.session_sequence_no = 0 + self.sequence_letter = -1 + self.downloads_today_tracker = downloads_today_tracker + self.stored_sequence_no = stored_sequence_no + self.matched_sequences = None + + def set_matched_sequence_value(self, matched_sequences): + self.matched_sequences = matched_sequences + + def get_session_sequence_no(self): + if self.matched_sequences is not None: + return self.matched_sequences.session_sequence_no + else: + return self._get_session_sequence_no() + + def _get_session_sequence_no(self): + return self.session_sequence_no + 1 + + def get_sequence_letter(self): + if self.matched_sequences is not None: + return self.matched_sequences.sequence_letter + else: + return self._get_sequence_letter() + + def _get_sequence_letter(self): + return self.sequence_letter + 1 + + def increment(self, uses_session_sequece_no, uses_sequence_letter): + if uses_session_sequece_no: + self.session_sequence_no += 1 + if uses_sequence_letter: + self.sequence_letter += 1 + + def get_downloads_today(self): + if self.matched_sequences is not None: + return self.matched_sequences.downloads_today + else: + return self._get_downloads_today() + + def _get_downloads_today(self): + v = self.downloads_today_tracker.get_downloads_today() + if v == -1: + return 1 + else: + return v + 1 + + def get_stored_sequence_no(self): + if self.matched_sequences is not None: + return self.matched_sequences.stored_sequence_no + else: + return self._get_stored_sequence_no() + + def _get_stored_sequence_no(self): + # Must add 1 to the value, for historic reasons (that is how it used + # to work) + return self.stored_sequence_no + 1 + + def create_matched_sequences(self): + sequences = collections.namedtuple( + 'AssignedSequences', + 'session_sequence_no sequence_letter downloads_today stored_sequence_no' + ) + sequences.session_sequence_no = self._get_session_sequence_no() + sequences.sequence_letter = self._get_sequence_letter() + sequences.downloads_today = self._get_downloads_today() + sequences.stored_sequence_no = self._get_stored_sequence_no() + return sequences diff --git a/rapid/generatenameconfig.py b/rapid/generatenameconfig.py new file mode 100644 index 0000000..321761e --- /dev/null +++ b/rapid/generatenameconfig.py @@ -0,0 +1,482 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# Special key in each dictionary which specifies the order of elements. +# It is very important to have a consistent and rational order when displaying +# these prefs to the user, and dictionaries are unsorted. + +import os + +from gettext import gettext as _ + +ORDER_KEY = "__order__" + +# PLEASE NOTE: these values are duplicated in a dummy class whose function +# is to have them put into the translation template. If you change the values below +# then you MUST change the value in class i18TranslateMeThanks as well!! + +# *** Level 0 +DATE_TIME = 'Date time' +TEXT = 'Text' +FILENAME = 'Filename' +METADATA = 'Metadata' +SEQUENCES = 'Sequences' +JOB_CODE = 'Job code' + +SEPARATOR = os.sep + +# *** Level 1 + +# Date time +IMAGE_DATE = 'Image date' +TODAY = 'Today' +YESTERDAY = 'Yesterday' +VIDEO_DATE = 'Video date' +DOWNLOAD_TIME = 'Download time' + +# File name +NAME_EXTENSION = 'Name + extension' +NAME = 'Name' +EXTENSION = 'Extension' +IMAGE_NUMBER = 'Image number' +VIDEO_NUMBER = 'Video number' + +# Metadata +APERTURE = 'Aperture' +ISO = 'ISO' +EXPOSURE_TIME = 'Exposure time' +FOCAL_LENGTH = 'Focal length' +CAMERA_MAKE = 'Camera make' +CAMERA_MODEL = 'Camera model' +SHORT_CAMERA_MODEL = 'Short camera model' +SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model' +SERIAL_NUMBER = 'Serial number' +SHUTTER_COUNT = 'Shutter count' +OWNER_NAME = 'Owner name' + +# Video metadata +CODEC = 'Codec' +WIDTH = 'Width' +HEIGHT = 'Height' +FPS = 'Frames Per Second' +LENGTH = 'Length' + +#Image sequences +DOWNLOAD_SEQ_NUMBER = 'Downloads today' +SESSION_SEQ_NUMBER = 'Session number' +SUBFOLDER_SEQ_NUMBER = 'Subfolder number' +STORED_SEQ_NUMBER = 'Stored number' + +SEQUENCE_LETTER = 'Sequence letter' + + + +# *** Level 2 + +# Image number +IMAGE_NUMBER_ALL = 'All digits' +IMAGE_NUMBER_1 = 'Last digit' +IMAGE_NUMBER_2 = 'Last 2 digits' +IMAGE_NUMBER_3 = 'Last 3 digits' +IMAGE_NUMBER_4 = 'Last 4 digits' + + +# Case +ORIGINAL_CASE = "Original Case" +UPPERCASE = "UPPERCASE" +LOWERCASE = "lowercase" + +# Sequence number +SEQUENCE_NUMBER_1 = "One digit" +SEQUENCE_NUMBER_2 = "Two digits" +SEQUENCE_NUMBER_3 = "Three digits" +SEQUENCE_NUMBER_4 = "Four digits" +SEQUENCE_NUMBER_5 = "Five digits" +SEQUENCE_NUMBER_6 = "Six digits" +SEQUENCE_NUMBER_7 = "Seven digits" + + +# Now, define dictionaries and lists of valid combinations of preferences. + +# Level 2 + +# Date + +SUBSECONDS = 'Subseconds' + +# ****** NOTE 1: if changing LIST_DATE_TIME_L2, you MUST update the default subfolder preference below ***** +# ****** NOTE 2: if changing LIST_DATE_TIME_L2, you MUST update DATE_TIME_CONVERT below ***** +LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', + 'MMDDYYYY', 'MMDDYY', 'MMDD', + 'DDMMYYYY', 'DDMMYY', 'YYYY', 'YY', + 'MM', 'DD', + 'HHMMSS', 'HHMM', 'HH-MM-SS', 'HH-MM', 'HH', 'MM (minutes)', 'SS'] + + +LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS] + +DEFAULT_SUBFOLDER_PREFS = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] +DEFAULT_VIDEO_SUBFOLDER_PREFS = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]] + +class i18TranslateMeThanks: + """ this class is never used in actual running code + It's purpose is to have these values inserted into the program's i18n template file + + """ + def __init__(self): + _('Date time') + _('Text') + _('Filename') + _('Metadata') + _('Sequences') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + _('Job code') + _('Image date') + _('Video date') + _('Today') + _('Yesterday') + # Translators: Download time is the time and date that the download started (when the user clicked the Download button) + _('Download time') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Name + extension') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Name') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Extension') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Image number') + _('Video number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Aperture') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('ISO') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Exposure time') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Focal length') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Camera make') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Short camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Hyphenated short camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Serial number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Shutter count') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Owner name') + _('Codec') + _('Width') + _('Height') + _('Length') + _('Frames Per Second') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Downloads today') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Session number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Subfolder number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Stored number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters + _('Sequence letter') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('All digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last digit') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 2 digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 3 digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 4 digits') + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("Original Case") + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("UPPERCASE") + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("lowercase") + _("One digit") + _("Two digits") + _("Three digits") + _("Four digits") + _("Five digits") + _("Six digits") + _("Seven digits") + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('Subseconds') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYYMMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYY-MM-DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYMMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YY-MM-DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDDYYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDDYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DDMMYYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DDMMYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HHMMSS') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HHMM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HH-MM-SS') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HH-MM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HH') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MM (minutes)') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('SS') + + +# Convenience values for python datetime conversion using values in +# LIST_DATE_TIME_L2. Obviously the two must remain synchronized. + +DATE_TIME_CONVERT = ['%Y%m%d', '%Y-%m-%d','%y%m%d', '%y-%m-%d', + '%m%d%Y', '%m%d%y', '%m%d', + '%d%m%Y', '%d%m%y', '%Y', '%y', + '%m', '%d', + '%H%M%S', '%H%M', '%H-%M-%S', '%H-%M', + '%H', '%M', '%S'] + + +LIST_IMAGE_NUMBER_L2 = [IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, + IMAGE_NUMBER_3, IMAGE_NUMBER_4] + + +LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE] + +LIST_SEQUENCE_LETTER_L2 = [ + UPPERCASE, + LOWERCASE + ] + + + +LIST_SEQUENCE_NUMBERS_L2 = [ + SEQUENCE_NUMBER_1, + SEQUENCE_NUMBER_2, + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + SEQUENCE_NUMBER_7, + ] + + + +LIST_SHUTTER_COUNT_L2 = [ + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + ] + +# Level 1 +LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] +LIST_VIDEO_DATE_TIME_L1 = [VIDEO_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] + +DICT_DATE_TIME_L1 = { + IMAGE_DATE: LIST_IMAGE_DATE_TIME_L2, + TODAY: LIST_DATE_TIME_L2, + YESTERDAY: LIST_DATE_TIME_L2, + DOWNLOAD_TIME: LIST_DATE_TIME_L2, + ORDER_KEY: LIST_DATE_TIME_L1 + } + +VIDEO_DICT_DATE_TIME_L1 = { + VIDEO_DATE: LIST_IMAGE_DATE_TIME_L2, + TODAY: LIST_DATE_TIME_L2, + YESTERDAY: LIST_DATE_TIME_L2, + DOWNLOAD_TIME: LIST_DATE_TIME_L2, + ORDER_KEY: LIST_VIDEO_DATE_TIME_L1 + } + + +LIST_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, IMAGE_NUMBER] + +DICT_FILENAME_L1 = { + NAME_EXTENSION: LIST_CASE_L2, + NAME: LIST_CASE_L2, + EXTENSION: LIST_CASE_L2, + IMAGE_NUMBER: LIST_IMAGE_NUMBER_L2, + ORDER_KEY: LIST_FILENAME_L1 + } + +LIST_VIDEO_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, VIDEO_NUMBER] + +DICT_VIDEO_FILENAME_L1 = { + NAME_EXTENSION: LIST_CASE_L2, + NAME: LIST_CASE_L2, + EXTENSION: LIST_CASE_L2, + VIDEO_NUMBER: LIST_IMAGE_NUMBER_L2, + ORDER_KEY: LIST_VIDEO_FILENAME_L1 + } + + +LIST_SUBFOLDER_FILENAME_L1 = [EXTENSION] + +DICT_SUBFOLDER_FILENAME_L1 = { + EXTENSION: LIST_CASE_L2, + ORDER_KEY: LIST_SUBFOLDER_FILENAME_L1 +} + +LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, + CAMERA_MAKE, CAMERA_MODEL, + SHORT_CAMERA_MODEL, + SHORT_CAMERA_MODEL_HYPHEN, + SERIAL_NUMBER, + SHUTTER_COUNT, + OWNER_NAME] + +LIST_VIDEO_METADATA_L1 = [CODEC, WIDTH, HEIGHT, LENGTH, FPS] + +DICT_METADATA_L1 = { + APERTURE: None, + ISO: None, + EXPOSURE_TIME: None, + FOCAL_LENGTH: None, + CAMERA_MAKE: LIST_CASE_L2, + CAMERA_MODEL: LIST_CASE_L2, + SHORT_CAMERA_MODEL: LIST_CASE_L2, + SHORT_CAMERA_MODEL_HYPHEN: LIST_CASE_L2, + SERIAL_NUMBER: None, + SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, + OWNER_NAME: LIST_CASE_L2, + ORDER_KEY: LIST_METADATA_L1 + } + +DICT_VIDEO_METADATA_L1 = { + CODEC: LIST_CASE_L2, + WIDTH: None, + HEIGHT: None, + LENGTH: None, + FPS: None, + ORDER_KEY: LIST_VIDEO_METADATA_L1 + } + +LIST_SEQUENCE_L1 = [ + DOWNLOAD_SEQ_NUMBER, + STORED_SEQ_NUMBER, + SESSION_SEQ_NUMBER, + SEQUENCE_LETTER + ] + +DICT_SEQUENCE_L1 = { + DOWNLOAD_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + STORED_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + SESSION_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + SEQUENCE_LETTER: LIST_SEQUENCE_LETTER_L2, + ORDER_KEY: LIST_SEQUENCE_L1 + } + + +# Level 0 + + +LIST_IMAGE_RENAME_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, + SEQUENCES, JOB_CODE] + +LIST_VIDEO_RENAME_L0 = LIST_IMAGE_RENAME_L0 + + +DICT_IMAGE_RENAME_L0 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_FILENAME_L1, + METADATA: DICT_METADATA_L1, + SEQUENCES: DICT_SEQUENCE_L1, + JOB_CODE: None, + ORDER_KEY: LIST_IMAGE_RENAME_L0 + } + +DICT_VIDEO_RENAME_L0 = { + DATE_TIME: VIDEO_DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_VIDEO_FILENAME_L1, + METADATA: DICT_VIDEO_METADATA_L1, + SEQUENCES: DICT_SEQUENCE_L1, + JOB_CODE: None, + ORDER_KEY: LIST_VIDEO_RENAME_L0 + } + +LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] + +DICT_SUBFOLDER_L0 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_SUBFOLDER_FILENAME_L1, + METADATA: DICT_METADATA_L1, + JOB_CODE: None, + SEPARATOR: None, + ORDER_KEY: LIST_SUBFOLDER_L0 + } + +LIST_VIDEO_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] + +DICT_VIDEO_SUBFOLDER_L0 = { + DATE_TIME: VIDEO_DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_SUBFOLDER_FILENAME_L1, + METADATA: DICT_VIDEO_METADATA_L1, + JOB_CODE: None, + SEPARATOR: None, + ORDER_KEY: LIST_VIDEO_SUBFOLDER_L0 + } + +# preference elements that require metadata +# note there is no need to specify lower level elements if a higher level +# element is necessary for them to be present to begin with +METADATA_ELEMENTS = [METADATA, IMAGE_DATE] + +# preference elements that are sequence numbers or letters +SEQUENCE_ELEMENTS = [ + DOWNLOAD_SEQ_NUMBER, + SESSION_SEQ_NUMBER, + SUBFOLDER_SEQ_NUMBER, + STORED_SEQ_NUMBER, + SEQUENCE_LETTER] + +# preference elements that do not require metadata and are not fixed +# as above, there is no need to specify lower level elements if a higher level +# element is necessary for them to be present to begin with +DYNAMIC_NON_METADATA_ELEMENTS = [ + TODAY, YESTERDAY, + FILENAME] + SEQUENCE_ELEMENTS diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui new file mode 100644 index 0000000..ef8a65b --- /dev/null +++ b/rapid/glade3/about.ui @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.20"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkAboutDialog" id="about"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="destroy_with_parent">True</property> + <property name="icon">rapid-photo-downloader.svg</property> + <property name="type_hint">dialog</property> + <property name="program_name">Rapid Photo Downloader</property> + <property name="copyright">Copyright Damon Lynch 2007-11</property> + <property name="comments" translatable="yes">Import your photos and videos efficiently and reliably</property> + <property name="website">http://www.damonlynch.net/rapid</property> + <property name="license">Rapid Photo Downloader 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. + +Rapid Photo Downloader 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 Rapid Photo Downloader; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</property> + <property name="authors">Damon Lynch <damonlynch@gmail.com></property> + <property name="translator_credits">Anton Alyab'ev <subeditor@dolgopa.org> +Lőrincz András <level.andrasnak@gmail.com> +Michel Ange <michelange@wanadoo.fr> +Alain J. Baudrez <a.baudrez@gmail.com> +Bert <crinbert@yahoo.com> +Martin Dahl Moe +Martin Egger <martin.egger@gmx.net> +Miroslav Matejaš <silverspace@ubuntu-hr.org> +Nicolás M. Zahlut <nzahlut@live.com> +Erik M +Jose Luis Navarro <jlnavarro111@gmail.com> +Tomas Novak <kuvaly@seznam.cz> +Abel O'Rian <abel.orian@gmail.com> +Balazs Oveges <ovegesb@freemail.hu> +Daniel Paessler <daniel@paessler.org> +Miloš Popović <gpopac@gmail.com> +Michal Predotka <mpredotka@googlemail.com> +Ye Qing <allen19920930@gmail.com> +Luca Reverberi <thereve@gmail.com> +Mikko Ruohola <polarfox@polarfox.net> +Sergiy Gavrylov <sergiovana@bigmir.net> +Sergei Sedov <sedov@webmail.perm.ru> +Marco Solari <marcosolari@gmail.com> +Toni Lähdekorpi <toni@lygon.net> +Ulf Urdén <ulf.urden@purplescout.com> +Julien Valroff <julien@kirya.net> +Aron Xu <happyaron.xu@gmail.com> +梁其学 <yalongbay@gmail.com></property> + <property name="logo">rapid-photo-downloader.svg</property> + <property name="wrap_license">True</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> +</interface> diff --git a/rapid/glade3/errorlog.ui b/rapid/glade3/errorlog.ui new file mode 100644 index 0000000..43c8bdd --- /dev/null +++ b/rapid/glade3/errorlog.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.20"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkDialog" id="errorlog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Error Log</property> + <property name="default_width">650</property> + <property name="default_height">400</property> + <property name="destroy_with_parent">True</property> + <property name="icon">rapid-photo-downloader.svg</property> + <property name="type_hint">dialog</property> + <signal name="response" handler="on_errorlog_response" swapped="no"/> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkButton" id="button1"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="log_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTextView" id="log_textview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="editable">False</property> + <property name="cursor_visible">False</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/rapid/glade3/media-eject.png b/rapid/glade3/media-eject.png Binary files differnew file mode 100644 index 0000000..0ff107e --- /dev/null +++ b/rapid/glade3/media-eject.png diff --git a/rapid/glade3/photo.png b/rapid/glade3/photo.png Binary files differdeleted file mode 100644 index b8bd550..0000000 --- a/rapid/glade3/photo.png +++ /dev/null diff --git a/rapid/glade3/photo.svg b/rapid/glade3/photo.svg new file mode 100644 index 0000000..95c57d0 --- /dev/null +++ b/rapid/glade3/photo.svg @@ -0,0 +1,1208 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + width="96" + height="96" + id="svg2408" + inkscape:version="0.48.1 r9760" + sodipodi:docname="camera-photo.svg" + inkscape:export-filename="/home/damon/rapid/branding/camera-photo.png" + inkscape:export-xdpi="69.209999" + inkscape:export-ydpi="69.209999"> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1176" + id="namedview245" + showgrid="false" + inkscape:zoom="7.719249" + inkscape:cx="32.454446" + inkscape:cy="48" + inkscape:window-x="0" + inkscape:window-y="24" + inkscape:window-maximized="1" + inkscape:current-layer="svg2408" /> + <defs + id="defs2410"> + <linearGradient + id="linearGradient3921"> + <stop + id="stop3923" + style="stop-color:#5fd3bc;stop-opacity:0" + offset="0" /> + <stop + id="stop3929" + style="stop-color:#5fd3bc;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3925" + style="stop-color:#5fd3bc;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3911"> + <stop + id="stop3913" + style="stop-color:#2a7fff;stop-opacity:0" + offset="0" /> + <stop + id="stop3919" + style="stop-color:#2a7fff;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3915" + style="stop-color:#2a7fff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3901"> + <stop + id="stop3903" + style="stop-color:#6600ff;stop-opacity:0" + offset="0" /> + <stop + id="stop3909" + style="stop-color:#6600ff;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3905" + style="stop-color:#6600ff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3891"> + <stop + id="stop3893" + style="stop-color:#d42aff;stop-opacity:0" + offset="0" /> + <stop + id="stop3899" + style="stop-color:#d42aff;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3895" + style="stop-color:#d42aff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3881"> + <stop + id="stop3883" + style="stop-color:#d40000;stop-opacity:0" + offset="0" /> + <stop + id="stop3889" + style="stop-color:#d40000;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3885" + style="stop-color:#d40000;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3871"> + <stop + id="stop3873" + style="stop-color:#ff6600;stop-opacity:0" + offset="0" /> + <stop + id="stop3879" + style="stop-color:#ff6600;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3875" + style="stop-color:#ff6600;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3861"> + <stop + id="stop3863" + style="stop-color:#ffcc00;stop-opacity:0" + offset="0" /> + <stop + id="stop3869" + style="stop-color:#ffcc00;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3865" + style="stop-color:#ffcc00;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3851"> + <stop + id="stop3853" + style="stop-color:#55d400;stop-opacity:0" + offset="0" /> + <stop + id="stop3859" + style="stop-color:#55d400;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3855" + style="stop-color:#55d400;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3797"> + <stop + id="stop3799" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3805" + style="stop-color:#ffffff;stop-opacity:1" + offset="0.5" /> + <stop + id="stop3801" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3761"> + <stop + id="stop3763" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3765" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3727"> + <stop + id="stop3729" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3731" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3681"> + <stop + id="stop3683" + style="stop-color:#ffffff;stop-opacity:0.18548387" + offset="0" /> + <stop + id="stop3689" + style="stop-color:#ffffff;stop-opacity:0.10887097" + offset="0.3086735" /> + <stop + id="stop3691" + style="stop-color:#000000;stop-opacity:0" + offset="0.43622452" /> + <stop + id="stop3693" + style="stop-color:#000000;stop-opacity:0" + offset="0.54464293" /> + <stop + id="stop3695" + style="stop-color:#ffffff;stop-opacity:0" + offset="0.58290821" /> + <stop + id="stop3697" + style="stop-color:#000000;stop-opacity:0.1814516" + offset="0.61479598" /> + <stop + id="stop3699" + style="stop-color:#000000;stop-opacity:0.43145162" + offset="0.69132656" /> + <stop + id="stop3701" + style="stop-color:#ffffff;stop-opacity:0.08064516" + offset="0.78061229" /> + <stop + id="stop3703" + style="stop-color:#000000;stop-opacity:0" + offset="0.87627554" /> + <stop + id="stop3685" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3746"> + <stop + id="stop3748" + style="stop-color:#5d5d5d;stop-opacity:1" + offset="0" /> + <stop + id="stop3753" + style="stop-color:#0a0a0a;stop-opacity:1" + offset="0.60000002" /> + <stop + id="stop3751" + style="stop-color:#4e4e4e;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3732"> + <stop + id="stop3734" + style="stop-color:#000000;stop-opacity:0" + offset="0" /> + <stop + id="stop3740" + style="stop-color:#000000;stop-opacity:0" + offset="0.94999999" /> + <stop + id="stop3736" + style="stop-color:#000000;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3719"> + <stop + id="stop3721" + style="stop-color:#4b4b4b;stop-opacity:1" + offset="0" /> + <stop + id="stop3723" + style="stop-color:#232323;stop-opacity:1" + offset="0.37373093" /> + <stop + id="stop3725" + style="stop-color:#5c5c5c;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3664"> + <stop + id="stop3666" + style="stop-color:#000000;stop-opacity:0.68992245" + offset="0" /> + <stop + id="stop3804" + style="stop-color:#7f7f7f;stop-opacity:0" + offset="0.5" /> + <stop + id="stop3668" + style="stop-color:#ffffff;stop-opacity:0.21705426" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3650"> + <stop + id="stop3709" + style="stop-color:#141414;stop-opacity:1" + offset="0" /> + <stop + id="stop3654" + style="stop-color:#616161;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3641"> + <stop + id="stop3643" + style="stop-color:#0c0c0c;stop-opacity:1" + offset="0" /> + <stop + id="stop3645" + style="stop-color:#373737;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + x1="45.447727" + y1="92.539597" + x2="45.447727" + y2="7.0165396" + id="ButtonShadow" + gradientUnits="userSpaceOnUse" + gradientTransform="scale(1.0058652,0.994169)"> + <stop + id="stop3750" + style="stop-color:#000000;stop-opacity:1" + offset="0" /> + <stop + id="stop3752" + style="stop-color:#000000;stop-opacity:0.58823532" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3737"> + <stop + id="stop3739" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3741" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <filter + color-interpolation-filters="sRGB" + id="filter3174"> + <feGaussianBlur + id="feGaussianBlur3176" + stdDeviation="1.71" /> + </filter> + <linearGradient + x1="36.357143" + y1="6" + x2="36.357143" + y2="63.893143" + id="linearGradient3188" + xlink:href="#linearGradient3737" + gradientUnits="userSpaceOnUse" /> + <filter + x="-0.192" + y="-0.192" + width="1.3839999" + height="1.3839999" + color-interpolation-filters="sRGB" + id="filter3794"> + <feGaussianBlur + id="feGaussianBlur3796" + stdDeviation="5.28" /> + </filter> + <linearGradient + x1="48" + y1="20.220806" + x2="48" + y2="138.66119" + id="linearGradient3613" + xlink:href="#linearGradient3737" + gradientUnits="userSpaceOnUse" /> + <radialGradient + cx="48" + cy="90.171875" + r="42" + fx="48" + fy="90.171875" + id="radialGradient3619" + xlink:href="#linearGradient3737" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.5510206,0.19713193)" /> + <clipPath + id="clipPath3613"> + <rect + width="84" + height="84" + rx="6" + ry="6" + x="6" + y="6" + id="rect3615" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </clipPath> + <linearGradient + x1="48" + y1="90" + x2="48" + y2="5.9877172" + id="linearGradient3617" + xlink:href="#linearGradient3641" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="16.162441" + y1="52.098946" + x2="76.838295" + y2="52.098946" + id="linearGradient3670" + xlink:href="#linearGradient3664" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="16.162441" + y1="44.48222" + x2="76.771591" + y2="44.48222" + id="linearGradient3674" + xlink:href="#linearGradient3719" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="16.162441" + y1="44.48222" + x2="76.771591" + y2="44.48222" + id="linearGradient3707" + xlink:href="#linearGradient3650" + gradientUnits="userSpaceOnUse" /> + <radialGradient + cx="48" + cy="48.000935" + r="27.001867" + fx="48" + fy="48.000935" + id="radialGradient3738" + xlink:href="#linearGradient3732" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0369653,0,0,1.0369653,-1.774336,-1.7753043)" /> + <linearGradient + x1="16.162441" + y1="44.48222" + x2="76.771591" + y2="44.48222" + id="linearGradient3744" + xlink:href="#linearGradient3746" + gradientUnits="userSpaceOnUse" /> + <clipPath + id="clipPath3798"> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 1 1 -27.274118,0 13.637059,13.637059 0 1 1 27.274118,0 z" + transform="matrix(1.0266143,0,0,1.0266143,-0.999999,0.778362)" + id="path3800" + style="fill:#ff00ff;fill-opacity:1;stroke:none" /> + </clipPath> + <filter + x="-0.23999999" + y="-0.24000001" + width="1.48" + height="1.48" + color-interpolation-filters="sRGB" + id="filter3833"> + <feGaussianBlur + id="feGaussianBlur3835" + stdDeviation="2.8000002" /> + </filter> + <radialGradient + cx="47.729706" + cy="45.997448" + r="13.637059" + fx="47.729706" + fy="45.997448" + id="radialGradient3687" + xlink:href="#linearGradient3681" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="33.199345" + y1="45.997448" + x2="67.957489" + y2="45.997448" + id="linearGradient3733" + xlink:href="#linearGradient3727" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="40.068184" + y1="58.17598" + x2="31.793724" + y2="49.665066" + id="linearGradient3767" + xlink:href="#linearGradient3761" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="40.068184" + y1="58.17598" + x2="40.068184" + y2="41.697067" + id="linearGradient3771" + xlink:href="#linearGradient3761" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="40.068184" + y1="58.17598" + x2="40.068184" + y2="41.697067" + id="linearGradient3775" + xlink:href="#linearGradient3761" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="40.068184" + y1="58.17598" + x2="40.068184" + y2="41.697067" + id="linearGradient3779" + xlink:href="#linearGradient3761" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="40.068184" + y1="58.17598" + x2="40.068184" + y2="41.697067" + id="linearGradient3791" + xlink:href="#linearGradient3761" + gradientUnits="userSpaceOnUse" /> + <radialGradient + cx="41" + cy="52" + r="9.7082043" + fx="41" + fy="52" + id="radialGradient3803" + xlink:href="#linearGradient3797" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.61803397,0,19.862234)" /> + <filter + color-interpolation-filters="sRGB" + id="filter3774"> + <feGaussianBlur + id="feGaussianBlur3776" + stdDeviation="0.27274118" /> + </filter> + <linearGradient + x1="53.510502" + y1="59.608364" + x2="61.49099" + y2="39.92907" + id="linearGradient3967" + xlink:href="#linearGradient3881" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0266143,0,0,1.0266143,-0.9999987,50.778362)" /> + <linearGradient + x1="53.730709" + y1="60.139374" + x2="62.58559" + y2="38.831467" + id="linearGradient3969" + xlink:href="#linearGradient3891" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.72592593,0.72592593,-0.72592593,0.72592593,46.742509,29.961028)" /> + <linearGradient + x1="53.387505" + y1="60.34634" + x2="61.982975" + y2="39.437084" + id="linearGradient3971" + xlink:href="#linearGradient3901" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,1.0266143,-1.0266143,0,95.221638,49.000001)" /> + <linearGradient + x1="53.295853" + y1="59.182693" + x2="61.88982" + y2="40.223007" + id="linearGradient3973" + xlink:href="#linearGradient3911" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(-0.72592593,0.72592593,-0.72592593,-0.72592593,116.03897,96.742509)" /> + <linearGradient + x1="53.87949" + y1="61.330303" + x2="60.999012" + y2="39.683075" + id="linearGradient3975" + xlink:href="#linearGradient3921" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(-1.0266143,0,0,-1.0266143,96.999997,145.22164)" /> + <linearGradient + x1="53.469795" + y1="59.878456" + x2="60.324341" + y2="39.70118" + id="linearGradient3977" + xlink:href="#linearGradient3851" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(-0.72592593,-0.72592593,0.72592593,-0.72592593,49.25749,166.03897)" /> + <linearGradient + x1="61.628212" + y1="39.683075" + x2="52.881298" + y2="60.715324" + id="linearGradient3979" + xlink:href="#linearGradient3861" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.0266143,1.0266143,0,0.77836254,147)" /> + <linearGradient + x1="53.034939" + y1="61.183025" + x2="61.802849" + y2="40.483921" + id="linearGradient3981" + xlink:href="#linearGradient3871" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.72592593,-0.72592593,0.72592593,0.72592593,-20.03897,99.25749)" /> + <filter + color-interpolation-filters="sRGB" + id="filter3987"> + <feGaussianBlur + id="feGaussianBlur3989" + stdDeviation="0.42020394" /> + </filter> + <linearGradient + x1="64.072342" + y1="64.036171" + x2="46.604744" + y2="46.568573" + id="linearGradient3817" + xlink:href="#linearGradient3761" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="45.447727" + y1="92.539597" + x2="45.447727" + y2="7.0165396" + id="ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0058652,0,0,0.994169,100,0)"> + <stop + id="stop3750-8" + style="stop-color:#000000;stop-opacity:1" + offset="0" /> + <stop + id="stop3752-5" + style="stop-color:#000000;stop-opacity:0.58823532" + offset="1" /> + </linearGradient> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3780" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3772" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3725" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3721" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,-97)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient2918" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="15.999999" + y1="85.0625" + x2="20.178572" + y2="85.0625" + id="linearGradient3556" + xlink:href="#linearGradient3376" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3376"> + <stop + id="stop3378" + style="stop-color:#767676;stop-opacity:1" + offset="0" /> + <stop + id="stop3380" + style="stop-color:#fdfdfd;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3733-2"> + <stop + id="stop3735" + style="stop-color:#ffbf67;stop-opacity:1" + offset="0" /> + <stop + id="stop3737" + style="stop-color:#c70000;stop-opacity:1" + offset="1" /> + </linearGradient> + <radialGradient + cx="18.089285" + cy="85.0625" + r="2.0892856" + fx="18.089285" + fy="85.0625" + id="radialGradient3029" + xlink:href="#linearGradient3733-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,1.0042736,0,-0.3635233)" /> + </defs> + <metadata + id="metadata2413"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="layer2" + style="display:none"> + <rect + width="86" + height="85" + rx="6" + ry="6" + x="5" + y="7" + id="rect3745" + style="opacity:0.9;fill:url(#ButtonShadow);fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3174)" /> + </g> + <g + id="layer1" + style="display:inline" + inkscape:export-xdpi="70.709999" + inkscape:export-ydpi="70.709999" + inkscape:export-filename="/home/damon/rapid/branding/photo66.png"> + <rect + width="84" + height="84" + rx="6" + ry="6" + x="6" + y="6" + id="rect2419" + style="fill:url(#linearGradient3617);fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,2 0,68 0,2 c 0,0.334721 0.04135,0.6507 0.09375,0.96875 0.0487,0.295596 0.09704,0.596915 0.1875,0.875 0.00988,0.03038 0.020892,0.0636 0.03125,0.09375 0.098865,0.287771 0.2348802,0.547452 0.375,0.8125 0.1445918,0.273507 0.3156161,0.535615 0.5,0.78125 0.1843839,0.245635 0.3737765,0.473472 0.59375,0.6875 0.439947,0.428056 0.94291,0.814526 1.5,1.09375 0.278545,0.139612 0.5734731,0.246947 0.875,0.34375 -0.2562018,-0.100222 -0.4867109,-0.236272 -0.71875,-0.375 -0.00741,-0.0044 -0.023866,0.0045 -0.03125,0 -0.031933,-0.0193 -0.062293,-0.04251 -0.09375,-0.0625 -0.120395,-0.0767 -0.2310226,-0.163513 -0.34375,-0.25 -0.1061728,-0.0808 -0.2132809,-0.161112 -0.3125,-0.25 C 8.4783201,88.557317 8.3087904,88.373362 8.15625,88.1875 8.0486711,88.057245 7.9378561,87.922215 7.84375,87.78125 7.818661,87.74287 7.805304,87.69538 7.78125,87.65625 7.716487,87.553218 7.6510225,87.451733 7.59375,87.34375 7.4927417,87.149044 7.3880752,86.928049 7.3125,86.71875 7.30454,86.69694 7.288911,86.6782 7.28125,86.65625 7.2494249,86.5643 7.2454455,86.469419 7.21875,86.375 7.1884177,86.268382 7.1483606,86.171969 7.125,86.0625 7.0521214,85.720988 7,85.364295 7,85 L 7,83 7,15 7,13 C 7,10.218152 9.2181517,8 12,8 l 2,0 68,0 2,0 c 2.781848,0 5,2.218152 5,5 l 0,2 0,68 0,2 c 0,0.364295 -0.05212,0.720988 -0.125,1.0625 -0.04415,0.206893 -0.08838,0.397658 -0.15625,0.59375 -0.0077,0.02195 -0.0233,0.04069 -0.03125,0.0625 -0.06274,0.173739 -0.138383,0.367449 -0.21875,0.53125 -0.04158,0.0828 -0.07904,0.169954 -0.125,0.25 -0.0546,0.09721 -0.126774,0.18835 -0.1875,0.28125 -0.09411,0.140965 -0.204921,0.275995 -0.3125,0.40625 -0.143174,0.17445 -0.303141,0.346998 -0.46875,0.5 -0.01117,0.0102 -0.01998,0.02115 -0.03125,0.03125 -0.138386,0.125556 -0.285091,0.234436 -0.4375,0.34375 -0.102571,0.07315 -0.204318,0.153364 -0.3125,0.21875 -0.0074,0.0045 -0.02384,-0.0044 -0.03125,0 -0.232039,0.138728 -0.462548,0.274778 -0.71875,0.375 0.301527,-0.0968 0.596455,-0.204138 0.875,-0.34375 0.55709,-0.279224 1.060053,-0.665694 1.5,-1.09375 0.219973,-0.214028 0.409366,-0.441865 0.59375,-0.6875 0.184384,-0.245635 0.355408,-0.507743 0.5,-0.78125 0.14012,-0.265048 0.276135,-0.524729 0.375,-0.8125 0.01041,-0.03078 0.02133,-0.06274 0.03125,-0.09375 0.09046,-0.278085 0.1388,-0.579404 0.1875,-0.875 C 89.95865,84.6507 90,84.334721 90,84 l 0,-2 0,-68 0,-2 C 90,8.676 87.324,6 84,6 L 12,6 z" + inkscape:connector-curvature="0" + id="rect3728" + style="opacity:0.25;fill:url(#linearGradient3188);fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="M 12,90 C 8.676,90 6,87.324 6,84 L 6,82 6,14 6,12 c 0,-0.334721 0.04135,-0.6507 0.09375,-0.96875 0.0487,-0.295596 0.09704,-0.596915 0.1875,-0.875 C 6.29113,10.12587 6.302142,10.09265 6.3125,10.0625 6.411365,9.774729 6.5473802,9.515048 6.6875,9.25 6.8320918,8.976493 7.0031161,8.714385 7.1875,8.46875 7.3718839,8.223115 7.5612765,7.995278 7.78125,7.78125 8.221197,7.353194 8.72416,6.966724 9.28125,6.6875 9.559795,6.547888 9.8547231,6.440553 10.15625,6.34375 9.9000482,6.443972 9.6695391,6.580022 9.4375,6.71875 c -0.00741,0.0044 -0.023866,-0.0045 -0.03125,0 -0.031933,0.0193 -0.062293,0.04251 -0.09375,0.0625 -0.120395,0.0767 -0.2310226,0.163513 -0.34375,0.25 -0.1061728,0.0808 -0.2132809,0.161112 -0.3125,0.25 C 8.4783201,7.442683 8.3087904,7.626638 8.15625,7.8125 8.0486711,7.942755 7.9378561,8.077785 7.84375,8.21875 7.818661,8.25713 7.805304,8.30462 7.78125,8.34375 7.716487,8.446782 7.6510225,8.548267 7.59375,8.65625 7.4927417,8.850956 7.3880752,9.071951 7.3125,9.28125 7.30454,9.30306 7.288911,9.3218 7.28125,9.34375 7.2494249,9.4357 7.2454455,9.530581 7.21875,9.625 7.1884177,9.731618 7.1483606,9.828031 7.125,9.9375 7.0521214,10.279012 7,10.635705 7,11 l 0,2 0,68 0,2 c 0,2.781848 2.2181517,5 5,5 l 2,0 68,0 2,0 c 2.781848,0 5,-2.218152 5,-5 l 0,-2 0,-68 0,-2 C 89,10.635705 88.94788,10.279012 88.875,9.9375 88.83085,9.730607 88.78662,9.539842 88.71875,9.34375 88.71105,9.3218 88.69545,9.30306 88.6875,9.28125 88.62476,9.107511 88.549117,8.913801 88.46875,8.75 88.42717,8.6672 88.38971,8.580046 88.34375,8.5 88.28915,8.40279 88.216976,8.31165 88.15625,8.21875 88.06214,8.077785 87.951329,7.942755 87.84375,7.8125 87.700576,7.63805 87.540609,7.465502 87.375,7.3125 87.36383,7.3023 87.35502,7.29135 87.34375,7.28125 87.205364,7.155694 87.058659,7.046814 86.90625,6.9375 86.803679,6.86435 86.701932,6.784136 86.59375,6.71875 c -0.0074,-0.0045 -0.02384,0.0044 -0.03125,0 -0.232039,-0.138728 -0.462548,-0.274778 -0.71875,-0.375 0.301527,0.0968 0.596455,0.204138 0.875,0.34375 0.55709,0.279224 1.060053,0.665694 1.5,1.09375 0.219973,0.214028 0.409366,0.441865 0.59375,0.6875 0.184384,0.245635 0.355408,0.507743 0.5,0.78125 0.14012,0.265048 0.276135,0.524729 0.375,0.8125 0.01041,0.03078 0.02133,0.06274 0.03125,0.09375 0.09046,0.278085 0.1388,0.579404 0.1875,0.875 C 89.95865,11.3493 90,11.665279 90,12 l 0,2 0,68 0,2 c 0,3.324 -2.676,6 -6,6 l -72,0 z" + inkscape:connector-curvature="0" + id="path3615" + style="opacity:0.15;fill:url(#radialGradient3619);fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </g> + <g + id="layer5" + style="display:none"> + <rect + width="66" + height="66" + rx="12" + ry="12" + x="15" + y="15" + clip-path="url(#clipPath3613)" + id="rect3171" + style="opacity:0.5;fill:url(#linearGradient3613);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter3794)" /> + </g> + <g + id="layer3"> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,1.1879394,-1.1879394,0,100.84218,-7.2000033)" + id="path3660" + style="fill:url(#linearGradient3670);fill-opacity:1;stroke:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0.79333332,0.79333332,-0.79333332,0.79333332,46.425393,-24.15306)" + id="path3648" + style="fill:url(#linearGradient3674);fill-opacity:1;stroke:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,1.1136932,-1.1136932,0,97.53954,-3.75)" + id="path3672" + style="fill:none;stroke:#000000;stroke-width:0.44895676;stroke-miterlimit:4;stroke-dasharray:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,0.92395284,-0.92395284,0,89.099473,5.0666664)" + id="path3697" + style="fill:url(#linearGradient3707);fill-opacity:1;stroke:none" /> + <g + transform="matrix(0.92592592,0,0,0.92592592,3.555556,3.5555557)" + id="g3691" + style="opacity:0.2"> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,0.88270495,-0.88270495,0,87.264675,6.9833334)" + id="path3676" + style="fill:none;stroke:#000000;stroke-width:0.61175603;stroke-miterlimit:4;stroke-dasharray:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,0.81670832,-0.81670832,0,84.328998,10.05)" + id="path3678" + style="fill:none;stroke:#000000;stroke-width:0.66119081;stroke-miterlimit:4;stroke-dasharray:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,0.84970663,-0.84970663,0,85.796836,8.5166669)" + id="path3680" + style="fill:none;stroke:#000000;stroke-width:0.63551354;stroke-miterlimit:4;stroke-dasharray:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,0.78371,-0.78371,0,82.861159,11.583334)" + id="path3682" + style="fill:none;stroke:#000000;stroke-width:0.68903041;stroke-miterlimit:4;stroke-dasharray:none" /> + </g> + <path + d="m 48,20 c -7.16533,0 -14.332595,2.731639 -19.799557,8.19879 -10.933924,10.934302 -10.933924,28.666181 0,39.600483 10.933924,10.934303 28.66519,10.934303 39.599114,0 10.933924,-10.934302 10.933924,-28.666181 0,-39.600483 C 62.332595,22.731639 55.16533,20 48,20 z m 0,2.592503 c 6.501873,0 12.9917,2.492512 17.952462,7.453446 9.921524,9.921866 9.921524,25.9843 0,35.906166 -9.921523,9.921867 -25.983401,9.921867 -35.904924,0 -9.921524,-9.921866 -9.921524,-25.9843 0,-35.906166 C 35.0083,25.085015 41.498127,22.592503 48,22.592503 z" + inkscape:connector-curvature="0" + id="path3727" + style="opacity:0.6;fill:url(#radialGradient3738);fill-opacity:1;stroke:none" /> + <path + d="m 76.771595,44.48222 a 30.304577,30.304577 0 1 1 -60.609154,0 30.304577,30.304577 0 1 1 60.609154,0 z" + transform="matrix(0,0.64907743,-0.64907743,0,76.872405,17.839307)" + id="path3742" + style="fill:url(#linearGradient3744);fill-opacity:1;stroke:#1a1a1a;stroke-width:1.54064822;stroke-miterlimit:4;stroke-dasharray:none" /> + <path + d="m 20.178571,85.0625 a 2.0892856,2.0982144 0 1 1 -4.178572,0 2.0892856,2.0982144 0 1 1 4.178572,0 z" + transform="matrix(0,1.6709402,1.6638297,0,-126.03844,-14.717185)" + id="path3374" + style="opacity:0.4025974;fill:url(#linearGradient3556);fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m 20.178571,85.0625 a 2.0892856,2.0982144 0 1 1 -4.178572,0 2.0892856,2.0982144 0 1 1 4.178572,0 z" + transform="matrix(1.1923077,0,0,1.1872339,-6.0769275,-85.480158)" + id="path3364" + style="fill:url(#radialGradient3029);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.84049988;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.53181823;stroke-dasharray:none;stroke-dashoffset:0" /> + <g + transform="matrix(-0.5,0.8660254,-0.8660254,-0.5,113.57328,30.437809)" + id="text3845" + style="font-size:4px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.70995669;fill:#b3b3b3;fill-opacity:1;stroke:none;font-family:Liberation Sans;-inkscape-font-specification:Liberation Sans"> + <path + d="m 45.682364,77.430433 2.189583,0.08937 -0.01139,0.279065 -1.7628,2.097825 1.531928,0.06253 -0.01243,0.304434 -1.955403,-0.07981 0.01107,-0.271258 1.763118,-2.105631 -1.766108,-0.07208 0.01243,-0.304434" + inkscape:connector-curvature="0" + id="path3853" /> + <path + d="m 42.472517,78.425885 c 0.02879,-0.212908 0.08593,-0.402272 0.171412,-0.568093 0.0842,-0.165997 0.19274,-0.303081 0.325617,-0.411252 0.132881,-0.108172 0.287729,-0.184466 0.464543,-0.228884 0.176819,-0.04442 0.37168,-0.05223 0.584585,-0.02345 0.224521,0.03036 0.416587,0.0918 0.5762,0.184334 0.159441,0.09382 0.286829,0.210906 0.382165,0.351247 0.09516,0.141631 0.159443,0.302738 0.19284,0.483321 0.0334,0.180581 0.03623,0.373454 0.0085,0.57862 -0.02879,0.212905 -0.08446,0.401153 -0.167018,0.564745 -0.08256,0.163589 -0.189371,0.297621 -0.320437,0.402098 -0.131239,0.105763 -0.285092,0.179564 -0.461561,0.221404 -0.177931,0.04295 -0.375931,0.04968 -0.594001,0.0202 -0.216775,-0.02931 -0.404587,-0.08821 -0.563437,-0.176695 -0.160134,-0.08866 -0.288778,-0.201318 -0.385932,-0.33796 -0.09844,-0.136822 -0.165735,-0.295051 -0.20189,-0.47469 -0.03615,-0.179643 -0.04001,-0.374626 -0.01157,-0.58495 m 0.377425,0.05103 c -0.02215,0.163872 -0.02101,0.315785 0.0034,0.455739 0.02315,0.139777 0.06956,0.262991 0.139229,0.369644 0.06838,0.106475 0.159731,0.193719 0.274042,0.261734 0.114316,0.06801 0.250829,0.112747 0.409539,0.134208 0.162585,0.02198 0.308657,0.01545 0.438217,-0.01959 0.129562,-0.03504 0.242088,-0.09472 0.337579,-0.179039 0.0942,-0.0845 0.171021,-0.19105 0.230455,-0.319661 0.05815,-0.128789 0.0983,-0.275119 0.120452,-0.438991 0.02216,-0.163875 0.02184,-0.316991 -9.62e-4,-0.459349 -0.02426,-0.141244 -0.07032,-0.267039 -0.138182,-0.377386 -0.06932,-0.109232 -0.160878,-0.19979 -0.274669,-0.271674 -0.115254,-0.07077 -0.252238,-0.116881 -0.410951,-0.13834 -0.170324,-0.02303 -0.320529,-0.01509 -0.450615,0.02382 -0.131372,0.03874 -0.243045,0.101816 -0.335017,0.18924 -0.09343,0.08854 -0.168106,0.198668 -0.224021,0.330386 -0.0572,0.131541 -0.09671,0.277959 -0.118517,0.439253" + inkscape:connector-curvature="0" + id="path3855" /> + <path + d="m 39.301436,77.678216 c 0.05112,-0.208676 0.127935,-0.390946 0.230457,-0.546811 0.101263,-0.156174 0.223674,-0.281027 0.367232,-0.374559 0.143564,-0.09353 0.305604,-0.153043 0.48612,-0.178537 0.180521,-0.02549 0.375118,-0.01268 0.583792,0.03843 0.220058,0.0539 0.404561,0.135289 0.553507,0.244163 0.148639,0.110139 0.262948,0.240021 0.342928,0.389647 0.07967,0.150891 0.126575,0.317886 0.140711,0.500986 0.01414,0.183099 -0.0034,0.375192 -0.05268,0.57628 -0.05111,0.208673 -0.126357,0.389988 -0.225734,0.543945 -0.09938,0.153953 -0.219747,0.275954 -0.361115,0.366003 -0.141676,0.09131 -0.302464,0.148447 -0.482364,0.171414 -0.181473,0.02392 -0.379076,0.0097 -0.592812,-0.04265 -0.212467,-0.05205 -0.393008,-0.130451 -0.541622,-0.235219 -0.149874,-0.105082 -0.2659,-0.230693 -0.348077,-0.376833 -0.08344,-0.146453 -0.133644,-0.310906 -0.150623,-0.493359 -0.01697,-0.182456 -2.14e-4,-0.376756 0.05028,-0.5829 m 0.369924,0.09061 c -0.03934,0.160615 -0.05425,0.311799 -0.04474,0.453551 0.0083,0.14144 0.04139,0.268868 0.09941,0.382282 0.05676,0.113102 0.138377,0.209507 0.244865,0.289216 0.106493,0.0797 0.237517,0.138609 0.393072,0.176713 0.159354,0.03903 0.305298,0.04797 0.437834,0.02681 0.132539,-0.02116 0.250739,-0.06862 0.354602,-0.142382 0.1026,-0.07407 0.190243,-0.171917 0.262929,-0.293531 0.07142,-0.121927 0.126805,-0.263198 0.166146,-0.423814 0.03934,-0.160617 0.0552,-0.312911 0.04756,-0.456881 -0.0092,-0.143016 -0.04172,-0.272973 -0.09755,-0.38987 -0.0574,-0.115943 -0.138876,-0.215664 -0.244439,-0.299165 -0.107134,-0.08255 -0.238481,-0.14287 -0.39404,-0.180972 -0.166938,-0.04089 -0.317142,-0.04886 -0.45061,-0.02391 -0.134729,0.02464 -0.25244,0.07558 -0.353131,0.152797 -0.102262,0.07818 -0.18815,0.1798 -0.257665,0.304875 -0.07077,0.124764 -0.125524,0.26619 -0.164248,0.424278" + inkscape:connector-curvature="0" + id="path3857" /> + <path + d="m 36.93709,75.37391 -0.630016,1.724455 c -0.02278,0.06237 -0.04663,0.125747 -0.07154,0.190121 -0.0249,0.06437 -0.04841,0.123018 -0.07052,0.175938 -0.02523,0.06148 -0.05041,0.120902 -0.07552,0.178268 0.03991,-0.05612 0.08021,-0.111404 0.120894,-0.165851 0.03512,-0.04678 0.0734,-0.09656 0.114863,-0.14934 0.03979,-0.052 0.07758,-0.0985 0.113345,-0.139497 l 1.311189,-1.517182 0.245826,0.08981 0.03352,2.00846 c 6.37e-4,0.02103 7.68e-4,0.04533 3.91e-4,0.07292 -0.0016,0.02714 -0.0024,0.05595 -0.0025,0.08643 -0.0017,0.03126 -0.0028,0.06274 -0.0033,0.09444 -0.0022,0.03248 -0.0041,0.06435 -0.0058,0.09561 -0.0029,0.07241 -0.0073,0.147052 -0.01319,0.223915 0.0238,-0.06894 0.04676,-0.137485 0.06889,-0.205643 0.01856,-0.05838 0.03884,-0.119587 0.06085,-0.183629 0.02157,-0.06282 0.04106,-0.118081 0.05849,-0.165777 l 0.630016,-1.724456 0.31187,0.113939 -0.944354,2.584849 -0.460466,-0.168228 -0.03313,-2.043669 c 3.78e-4,-0.02759 0.0016,-0.06319 0.0037,-0.106789 0.0016,-0.04238 0.0043,-0.08576 0.008,-0.130144 0.0037,-0.04438 0.008,-0.08648 0.01287,-0.126299 0.0032,-0.03904 0.0065,-0.06902 0.01,-0.08993 -0.01082,0.01823 -0.02782,0.0439 -0.05101,0.07702 -0.02319,0.03312 -0.04827,0.06762 -0.07525,0.103511 -0.0282,0.03545 -0.05534,0.06989 -0.08142,0.103335 -0.02608,0.03345 -0.04848,0.06061 -0.0672,0.0815 l -1.32992,1.545688 -0.449459,-0.164206 0.944354,-2.584848 0.315538,0.115279" + inkscape:connector-curvature="0" + id="path3859" /> + <path + d="m 35.016377,74.508208 -1.277834,2.43729 -0.330392,-0.173219 1.136356,-2.167442 -1.231619,-0.645719 0.141478,-0.269849 1.562011,0.818939" + inkscape:connector-curvature="0" + id="path3861" /> + <path + d="m 33.052264,73.45796 -1.475751,2.322801 -1.762295,-1.119644 0.16339,-0.257173 1.447423,0.919595 0.473414,-0.745142 -1.348511,-0.856753 0.161296,-0.253876 1.34851,0.856752 0.514261,-0.809436 -1.515013,-0.962537 0.16339,-0.257173 1.829886,1.162586" + inkscape:connector-curvature="0" + id="path3863" /> + <path + d="m 29.39996,70.908274 -0.273684,2.754447 c 0.03706,-0.052 0.07321,-0.103884 0.108446,-0.155651 0.03068,-0.04377 0.06386,-0.08971 0.09956,-0.137827 0.03387,-0.04788 0.06478,-0.08983 0.09272,-0.125828 l 1.120895,-1.444173 0.262296,0.203582 -1.687331,2.173973 -0.342528,-0.265853 0.270922,-2.776371 c -0.03603,0.0528 -0.07207,0.105595 -0.108102,0.158391 -0.03147,0.0448 -0.06574,0.0932 -0.102804,0.145196 -0.03786,0.05303 -0.07475,0.102686 -0.11068,0.148973 l -1.106525,1.425657 -0.265382,-0.205977 1.687331,-2.173973 0.354871,0.275434" + inkscape:connector-curvature="0" + id="path3865" /> + <path + d="m 26.456345,69.279934 c 0.07932,-0.08449 0.169006,-0.146739 0.269069,-0.186749 0.09912,-0.0409 0.207358,-0.05537 0.324723,-0.04342 0.116476,0.0129 0.24074,0.05365 0.372792,0.122251 0.130213,0.06866 0.266519,0.169826 0.408917,0.303501 0.249675,0.23438 0.403294,0.46074 0.460857,0.67908 0.05756,0.218341 0.0278,0.427927 -0.08929,0.628758 L 27.890512,70.58874 c 0.03572,-0.06469 0.05987,-0.131317 0.07244,-0.199881 0.01257,-0.06856 0.01031,-0.140343 -0.0068,-0.215339 -0.01895,-0.07494 -0.05505,-0.153477 -0.108314,-0.235621 -0.05326,-0.08214 -0.128304,-0.168665 -0.225137,-0.259566 -0.08069,-0.07575 -0.160978,-0.138615 -0.240858,-0.188599 -0.08077,-0.04903 -0.158729,-0.08203 -0.233882,-0.09901 -0.07515,-0.01697 -0.146958,-0.01562 -0.215426,0.004 -0.07031,0.01972 -0.134868,0.06091 -0.193687,0.12357 -0.06149,0.06551 -0.09621,0.132926 -0.10415,0.202264 -0.0089,0.06845 0.0012,0.141361 0.0304,0.218739 0.02916,0.07738 0.07277,0.160287 0.130836,0.248727 0.05807,0.08844 0.122864,0.185879 0.194392,0.292317 0.04443,0.06492 0.08843,0.131236 0.132026,0.198947 0.04175,0.06777 0.07938,0.136128 0.112875,0.205079 0.03166,0.06901 0.05735,0.138666 0.07707,0.208973 0.01973,0.07031 0.02838,0.140042 0.02595,0.209207 -0.0033,0.07011 -0.01909,0.139239 -0.04734,0.207378 -0.02914,0.06909 -0.0749,0.136856 -0.137279,0.20331 -0.08912,0.09493 -0.184298,0.158276 -0.285546,0.190033 -0.103085,0.03181 -0.209301,0.03747 -0.318649,0.01698 -0.109344,-0.0205 -0.219891,-0.06444 -0.331643,-0.131842 -0.111746,-0.0674 -0.221732,-0.151897 -0.329959,-0.25349 -0.124359,-0.116745 -0.221327,-0.226524 -0.290903,-0.329339 -0.07046,-0.101866 -0.117802,-0.200778 -0.142025,-0.296735 -0.02421,-0.09596 -0.02718,-0.189824 -0.0089,-0.281597 0.01645,-0.09172 0.0494,-0.185796 0.09886,-0.282239 l 0.311825,0.204319 c -0.03216,0.06089 -0.05324,0.122356 -0.06326,0.184391 -0.01096,0.06114 -0.0081,0.123692 0.0087,0.187645 0.01676,0.06395 0.04833,0.130199 0.09471,0.198743 0.04544,0.06765 0.107549,0.138462 0.186341,0.21243 0.09304,0.08733 0.179377,0.152312 0.259021,0.194934 0.07781,0.04268 0.14954,0.06805 0.215195,0.07611 0.06471,0.0072 0.123878,-6.92e-4 0.177502,-0.02357 0.05179,-0.02282 0.09862,-0.05654 0.140506,-0.101162 0.05615,-0.05981 0.08777,-0.122988 0.09488,-0.189536 0.0053,-0.06649 -0.0057,-0.136614 -0.0329,-0.210369 -0.0272,-0.07376 -0.06648,-0.150814 -0.117845,-0.231175 -0.05136,-0.08036 -0.106992,-0.164733 -0.1669,-0.253115 -0.04834,-0.07217 -0.09576,-0.144362 -0.142256,-0.216586 -0.04834,-0.07217 -0.09071,-0.144982 -0.127115,-0.218447 -0.03729,-0.07251 -0.06723,-0.145262 -0.0898,-0.218243 -0.02352,-0.07387 -0.0346,-0.147675 -0.03325,-0.221412 0.0014,-0.07374 0.01747,-0.147018 0.04832,-0.219845 0.02991,-0.07372 0.07918,-0.147125 0.147798,-0.220224" + inkscape:connector-curvature="0" + id="path3867" /> + <path + d="m 18.630854,41.246205 0.610015,0.126766 -0.06756,0.325086 -0.610014,-0.126766 -0.263863,1.269749 -0.267718,-0.05563 -1.560346,-1.610929 0.07511,-0.36142 1.812833,0.37672 0.07868,-0.37863 0.271543,0.05643 -0.07868,0.37863 m -1.76374,-0.02739 c 0.0058,0.0039 0.01727,0.0129 0.03427,0.02707 0.01573,0.01391 0.03374,0.02962 0.05404,0.04714 0.0203,0.01752 0.0411,0.03581 0.06241,0.05486 0.0203,0.01752 0.03704,0.03297 0.05021,0.04634 l 0.873873,0.901738 c 0.0089,0.0085 0.02027,0.02084 0.0342,0.03703 0.01419,0.01492 0.02888,0.03061 0.04408,0.04706 0.01393,0.01619 0.02785,0.03239 0.04177,0.04858 0.01392,0.01619 0.02455,0.02904 0.03189,0.03854 l 0.197897,-0.952311 -1.424642,-0.296051" + inkscape:connector-curvature="0" + id="path3869" /> + <path + d="m 19.399774,40.683288 -0.238326,-0.06876 c -0.127964,-0.106035 -0.234444,-0.220778 -0.319439,-0.344229 -0.08588,-0.125063 -0.159246,-0.251257 -0.220086,-0.378581 -0.06209,-0.127684 -0.117659,-0.252132 -0.166711,-0.373345 -0.04869,-0.122462 -0.100265,-0.234917 -0.154731,-0.337365 -0.05446,-0.102444 -0.117193,-0.190335 -0.188189,-0.263672 -0.07063,-0.07458 -0.159744,-0.127399 -0.267336,-0.158443 -0.07506,-0.02166 -0.143991,-0.02935 -0.206787,-0.02307 -0.06404,0.0059 -0.121594,0.02455 -0.172651,0.05589 -0.05105,0.03135 -0.09463,0.07434 -0.130719,0.128974 -0.03698,0.05303 -0.06594,0.115819 -0.08687,0.188378 -0.01949,0.06756 -0.02852,0.13407 -0.02708,0.199533 0.0018,0.06421 0.01479,0.124881 0.03897,0.181997 0.02418,0.05712 0.06008,0.108811 0.107717,0.155078 0.04675,0.04466 0.105762,0.08201 0.177045,0.112064 l -0.131524,0.336087 c -0.09183,-0.03869 -0.173699,-0.09077 -0.245611,-0.15624 -0.07191,-0.06547 -0.12867,-0.143506 -0.17029,-0.234115 -0.04287,-0.09097 -0.06782,-0.192355 -0.07487,-0.304159 -0.0067,-0.113052 0.0093,-0.236511 0.04791,-0.370375 0.0379,-0.131359 0.08729,-0.243821 0.148161,-0.337385 0.06088,-0.09356 0.13207,-0.166528 0.213583,-0.218903 0.08188,-0.05362 0.173268,-0.0862 0.27417,-0.09775 0.100906,-0.01154 0.210158,-3.48e-4 0.327755,0.03358 0.08883,0.02563 0.16858,0.06626 0.239262,0.121883 0.07105,0.05438 0.135087,0.118933 0.192126,0.193661 0.0574,0.07348 0.109775,0.154997 0.157118,0.244545 0.04734,0.08955 0.09253,0.181869 0.135571,0.276951 0.04179,0.09472 0.08339,0.190072 0.124816,0.286044 0.04018,0.09561 0.08224,0.187029 0.126188,0.274244 0.04431,0.08597 0.09186,0.165412 0.142647,0.238337 0.04953,0.07256 0.105625,0.134147 0.168272,0.184746 l 0.397946,-1.379288 0.287117,0.08284 -0.505148,1.750851" + inkscape:connector-curvature="0" + id="path3871" /> + <path + d="m 20.097629,38.396989 -0.403551,-0.141786 0.126248,-0.359327 0.403551,0.141787 -0.126248,0.359326" + inkscape:connector-curvature="0" + id="path3873" /> + <path + d="m 20.293152,35.445622 c 0.127403,0.05309 0.235975,0.1209 0.325717,0.203432 0.08974,0.08254 0.157297,0.17769 0.202668,0.285461 0.04417,0.107275 0.06375,0.226165 0.05874,0.356672 -0.0045,0.129308 -0.03757,0.267879 -0.09917,0.415713 -0.05559,0.133413 -0.119797,0.243488 -0.192615,0.330225 -0.07232,0.08554 -0.149793,0.151289 -0.232426,0.19726 -0.08384,0.04547 -0.170925,0.07336 -0.26127,0.08368 -0.08984,0.0091 -0.180338,0.0046 -0.271484,-0.01368 l 0.09886,-0.343898 c 0.05359,0.0068 0.108224,0.0077 0.163913,0.0027 0.05449,-0.0055 0.108273,-0.02118 0.161359,-0.04702 0.05238,-0.02754 0.101914,-0.06685 0.14859,-0.117936 0.04597,-0.05278 0.08699,-0.122443 0.123051,-0.208981 0.03506,-0.08413 0.05424,-0.165713 0.05755,-0.244741 0.0021,-0.07953 -0.01207,-0.153845 -0.04251,-0.222957 -0.02995,-0.07031 -0.07607,-0.133965 -0.138367,-0.190959 -0.0623,-0.05699 -0.140923,-0.10527 -0.235874,-0.144837 -0.07812,-0.03255 -0.154897,-0.04973 -0.230318,-0.05154 -0.07662,-0.0023 -0.148136,0.01022 -0.214544,0.03755 -0.06761,0.02684 -0.128907,0.06901 -0.183897,0.126499 -0.05449,0.05629 -0.09976,0.127704 -0.135822,0.21424 -0.02254,0.05409 -0.03851,0.10597 -0.04793,0.155648 -0.0094,0.04968 -0.01397,0.09786 -0.01367,0.144531 8.02e-4,0.04547 0.0059,0.08919 0.01517,0.13116 0.0086,0.04027 0.01918,0.07923 0.0317,0.116888 l -0.132218,0.317305 -1.331266,-0.654175 0.601743,-1.444098 0.275839,0.11494 -0.47854,1.148427 0.785599,0.384482 c -0.02043,-0.07622 -0.02824,-0.164112 -0.02343,-0.263673 0.0036,-0.100058 0.0302,-0.209583 0.07979,-0.328574 0.05259,-0.126199 0.119146,-0.231766 0.199676,-0.316702 0.08053,-0.08493 0.171027,-0.148081 0.271488,-0.189447 0.09926,-0.04186 0.205581,-0.06174 0.318962,-0.05964 0.113381,0.0021 0.228365,0.02745 0.34495,0.07603" + inkscape:connector-curvature="0" + id="path3875" /> + <path + d="M 21.868617,34.296638 20.68612,33.666653 c -0.09078,-0.04836 -0.169691,-0.08303 -0.23672,-0.103985 -0.06703,-0.02095 -0.126275,-0.02744 -0.177743,-0.01945 -0.052,0.0062 -0.09711,0.0272 -0.135312,0.06291 -0.03935,0.0351 -0.07587,0.08425 -0.109539,0.147452 -0.0349,0.06551 -0.05321,0.131728 -0.05494,0.198669 -0.0029,0.06633 0.01028,0.131622 0.0395,0.19587 0.02867,0.06249 0.0737,0.123359 0.135077,0.182614 0.06084,0.05749 0.137803,0.111038 0.230885,0.160627 l 1.080796,0.575804 -0.164385,0.308552 -1.466916,-0.781513 c -0.03907,-0.02081 -0.08017,-0.04197 -0.123305,-0.06348 -0.04367,-0.02326 -0.08477,-0.04442 -0.123305,-0.06348 -0.03968,-0.01966 -0.07362,-0.03627 -0.101815,-0.04982 -0.02819,-0.01354 -0.04662,-0.02188 -0.05527,-0.02502 l 0.156119,-0.293038 c 0.0064,0.0019 0.02248,0.009 0.04838,0.02135 0.02589,0.01232 0.05581,0.02678 0.08975,0.04339 0.0334,0.01485 0.06765,0.03088 0.102733,0.04809 0.03394,0.01661 0.0624,0.03103 0.08538,0.04328 l 0.0028,-0.0052 c -0.03932,-0.05635 -0.07224,-0.112252 -0.09877,-0.167695 -0.02652,-0.05544 -0.0434,-0.11238 -0.05063,-0.170819 -0.0078,-0.0602 -0.005,-0.122162 0.0083,-0.185895 0.01276,-0.06549 0.03874,-0.135011 0.07792,-0.20856 0.07531,-0.141345 0.162847,-0.237815 0.262623,-0.289409 0.100391,-0.05274 0.220638,-0.06244 0.360741,-0.02911 l 0.0028,-0.0052 c -0.03932,-0.05635 -0.07163,-0.1134 -0.09693,-0.171142 -0.0253,-0.05774 -0.04065,-0.11755 -0.04604,-0.179438 -0.0065,-0.06249 -0.0025,-0.126757 0.01198,-0.19279 0.01337,-0.06664 0.03965,-0.136733 0.07884,-0.210283 0.0502,-0.09423 0.106006,-0.168511 0.167409,-0.222846 0.06202,-0.05548 0.131087,-0.09097 0.207214,-0.106481 0.07613,-0.0155 0.161645,-0.01125 0.256554,0.01275 0.09376,0.0234 0.198099,0.06571 0.313016,0.126928 l 1.242829,0.662128 -0.163466,0.306829 -1.182497,-0.629986 c -0.09078,-0.04836 -0.169691,-0.08303 -0.236719,-0.103984 -0.06703,-0.02095 -0.126276,-0.02744 -0.177743,-0.01945 -0.052,0.0062 -0.09711,0.0272 -0.135312,0.06291 -0.03935,0.0351 -0.07587,0.08425 -0.109539,0.147452 -0.0349,0.06551 -0.05379,0.131423 -0.05667,0.197751 -0.004,0.06572 0.008,0.130399 0.03605,0.194033 0.02867,0.06249 0.0737,0.123361 0.135077,0.182614 0.06199,0.05811 0.140676,0.112569 0.236057,0.163382 l 1.080795,0.575804 -0.163465,0.306828" + inkscape:connector-curvature="0" + id="path3877" /> + <path + d="M 23.584114,31.437572 22.48027,30.678158 c -0.08475,-0.0583 -0.159236,-0.101646 -0.223471,-0.130035 -0.06424,-0.02839 -0.122372,-0.04151 -0.174413,-0.03939 -0.05237,3.21e-4 -0.09956,0.01607 -0.141546,0.04723 -0.04306,0.03043 -0.08489,0.07515 -0.12548,0.134149 -0.04207,0.06115 -0.06774,0.12488 -0.07701,0.191199 -0.01035,0.06558 -0.0046,0.131942 0.01714,0.199075 0.02144,0.06533 0.05931,0.130889 0.113605,0.196692 0.05396,0.06399 0.124392,0.12588 0.211282,0.185658 l 1.008907,0.6941 -0.198156,0.288029 -1.369346,-0.942072 c -0.03647,-0.02509 -0.07492,-0.05075 -0.115353,-0.07699 -0.04076,-0.02804 -0.07921,-0.05371 -0.115353,-0.07699 -0.03721,-0.02402 -0.06906,-0.04435 -0.09554,-0.06099 -0.02648,-0.01664 -0.04385,-0.027 -0.0521,-0.0311 l 0.188193,-0.273547 c 0.0061,0.0026 0.02132,0.01151 0.04566,0.02667 0.02434,0.01516 0.05243,0.03291 0.08428,0.05324 0.03151,0.01852 0.06373,0.03831 0.09665,0.05938 0.03185,0.02033 0.0585,0.03788 0.07995,0.05263 l 0.0033,-0.0048 c -0.0327,-0.06043 -0.05911,-0.119687 -0.07921,-0.17777 -0.0201,-0.05808 -0.03044,-0.11656 -0.03103,-0.175441 -9.19e-4,-0.06069 0.0088,-0.121945 0.02923,-0.18377 0.02007,-0.06363 0.05373,-0.129777 0.100956,-0.198434 0.09078,-0.131944 0.188646,-0.217918 0.293608,-0.257922 0.105701,-0.04107 0.226275,-0.03714 0.361722,0.01178 l 0.0033,-0.0048 c -0.03271,-0.06043 -0.05837,-0.120759 -0.077,-0.180988 -0.01862,-0.06022 -0.02712,-0.121386 -0.02549,-0.183486 5.57e-4,-0.06283 0.01178,-0.126235 0.03366,-0.190207 0.02081,-0.0647 0.05483,-0.131385 0.102063,-0.200043 0.06052,-0.08796 0.124348,-0.15547 0.19149,-0.202529 0.06788,-0.04812 0.140517,-0.0756 0.217907,-0.08242 0.07739,-0.0068 0.161882,0.0071 0.253476,0.04163 0.09052,0.03383 0.189418,0.08765 0.296692,0.161443 L 24.473049,30.145463 24.276,30.431884 23.172156,29.67247 c -0.08475,-0.0583 -0.159237,-0.101645 -0.223471,-0.130036 -0.06424,-0.02838 -0.122373,-0.04151 -0.174413,-0.03939 -0.05237,3.22e-4 -0.09956,0.01607 -0.141546,0.04723 -0.04306,0.03043 -0.08489,0.07515 -0.125481,0.134148 -0.04207,0.06115 -0.06827,0.124513 -0.07862,0.190092 -0.01142,0.06485 -0.0068,0.130467 0.01392,0.196861 0.02144,0.06533 0.05931,0.13089 0.113605,0.196692 0.05504,0.06473 0.127072,0.127726 0.216109,0.188979 l 1.008907,0.6941 -0.197049,0.28642" + inkscape:connector-curvature="0" + id="path3879" /> + <path + d="m 24.787912,28.352718 -0.23516,-0.205805 0.643142,-0.734876 0.235161,0.205805 -0.643143,0.734876" + inkscape:connector-curvature="0" + id="path3881" /> + <path + d="m 27.872659,25.20214 c 0.09453,0.100568 0.165856,0.206864 0.213976,0.318889 0.04812,0.112029 0.07083,0.226493 0.06814,0.343395 -0.0036,0.115955 -0.03437,0.23245 -0.09233,0.349485 -0.05702,0.116144 -0.143878,0.229062 -0.260572,0.338753 -0.105311,0.09899 -0.208932,0.173163 -0.310863,0.222515 -0.100984,0.04846 -0.19858,0.07676 -0.292789,0.0849 -0.0951,0.0072 -0.185981,-0.003 -0.272638,-0.03054 -0.08571,-0.02844 -0.166417,-0.06963 -0.242126,-0.123555 l 0.230914,-0.273348 c 0.04611,0.02814 0.09559,0.05132 0.148458,0.06953 0.05197,0.01727 0.107462,0.02497 0.166474,0.02311 0.05907,-0.0037 0.120346,-0.01931 0.183836,-0.04682 0.06355,-0.02935 0.129474,-0.07613 0.197784,-0.140346 0.06641,-0.06243 0.117291,-0.129016 0.152642,-0.199774 0.03446,-0.0717 0.05193,-0.145316 0.05243,-0.220836 0.0014,-0.07641 -0.0146,-0.153362 -0.04813,-0.230856 -0.03353,-0.07749 -0.08552,-0.153712 -0.15597,-0.228665 -0.05797,-0.06167 -0.120992,-0.108752 -0.189073,-0.141257 -0.06897,-0.03345 -0.13935,-0.05129 -0.211131,-0.05351 -0.07267,-0.0032 -0.145858,0.01023 -0.219556,0.04019 -0.07275,0.02907 -0.143278,0.07571 -0.211587,0.139918 -0.04269,0.04013 -0.0785,0.08094 -0.107417,0.122415 -0.02892,0.04148 -0.05279,0.08357 -0.07161,0.126285 -0.01787,0.04182 -0.03115,0.08378 -0.03982,0.125888 -0.0086,0.04027 -0.01491,0.08014 -0.0189,0.119626 l -0.250467,0.235436 -0.947096,-1.141588 1.139909,-1.071504 0.204669,0.217736 -0.906519,0.85212 0.559531,0.672247 c 0.01254,-0.07791 0.04138,-0.161302 0.0865,-0.250181 0.04423,-0.08982 0.113309,-0.178881 0.207234,-0.267172 0.09962,-0.09364 0.203542,-0.162733 0.311774,-0.207287 0.108231,-0.04455 0.216643,-0.06515 0.325236,-0.06179 0.107699,0.0024 0.212847,0.02777 0.315444,0.07608 0.102595,0.04831 0.197145,0.118481 0.283651,0.210507" + inkscape:connector-curvature="0" + id="path3883" /> + <path + d="m 29.355132,23.367427 c 0.166083,0.20563 0.286234,0.394797 0.360453,0.567502 0.07441,0.170877 0.112865,0.327279 0.115359,0.469207 0.0027,0.140099 -0.02642,0.265704 -0.08731,0.376815 -0.06089,0.111113 -0.143002,0.208395 -0.246321,0.291845 -0.104334,0.08427 -0.216712,0.144073 -0.337135,0.179409 -0.120424,0.03534 -0.248834,0.03695 -0.38523,0.0049 -0.135384,-0.03292 -0.278774,-0.103728 -0.430171,-0.212425 -0.151201,-0.110527 -0.310252,-0.269111 -0.477155,-0.475752 -0.174265,-0.215756 -0.299519,-0.40917 -0.375764,-0.580242 -0.07605,-0.1729 -0.114598,-0.328386 -0.115652,-0.466461 -0.001,-0.138069 0.03051,-0.260635 0.09467,-0.367698 0.06335,-0.108071 0.148709,-0.20547 0.25608,-0.292196 0.102308,-0.08263 0.21194,-0.142725 0.328895,-0.180285 0.117152,-0.03939 0.243107,-0.04404 0.377867,-0.01397 0.134761,0.03008 0.278657,0.100479 0.43169,0.211198 0.152214,0.109712 0.315454,0.272446 0.48972,0.4882 m -0.278053,0.224582 c -0.137449,-0.170173 -0.26171,-0.303297 -0.372783,-0.399373 -0.111892,-0.09708 -0.214588,-0.1631 -0.308091,-0.19805 -0.09432,-0.03596 -0.181395,-0.04429 -0.261223,-0.02501 -0.07963,0.01746 -0.155915,0.05564 -0.228847,0.114541 -0.07698,0.06218 -0.133396,0.131178 -0.16924,0.206991 -0.03584,0.07582 -0.04661,0.164021 -0.03232,0.264612 0.01449,0.09876 0.05743,0.213052 0.1288,0.342862 0.07056,0.128802 0.174149,0.277782 0.310779,0.446942 0.132541,0.164098 0.254347,0.294183 0.36542,0.390256 0.112087,0.09526 0.215699,0.16137 0.310838,0.198342 0.09533,0.03514 0.184141,0.04459 0.266424,0.02834 0.08146,-0.01726 0.159168,-0.05575 0.233113,-0.115476 0.07192,-0.05809 0.124884,-0.125137 0.158896,-0.201148 0.03319,-0.07702 0.04213,-0.16542 0.02682,-0.265196 -0.01512,-0.101605 -0.05745,-0.217217 -0.126988,-0.346836 -0.06853,-0.130434 -0.169065,-0.277701 -0.301605,-0.441799" + inkscape:connector-curvature="0" + id="path3885" /> + <path + d="M 31.579813,23.488032 30.839466,22.37131 c -0.05684,-0.08573 -0.109752,-0.153761 -0.158736,-0.204087 -0.04899,-0.05032 -0.09801,-0.08421 -0.147082,-0.101676 -0.0487,-0.01926 -0.09835,-0.02228 -0.148945,-0.009 -0.05131,0.01215 -0.106812,0.03801 -0.166501,0.07758 -0.06186,0.04101 -0.109475,0.09055 -0.142849,0.148601 -0.03409,0.05697 -0.05358,0.120664 -0.05845,0.191071 -0.0045,0.06861 0.0061,0.143568 0.03193,0.224888 0.02616,0.07952 0.06838,0.163229 0.126655,0.251133 l 0.676673,1.020677 -0.29139,0.193181 -0.918418,-1.385321 c -0.02446,-0.0369 -0.05055,-0.07506 -0.07825,-0.1145 -0.02734,-0.04124 -0.05342,-0.07941 -0.07825,-0.1145 -0.02555,-0.03618 -0.0475,-0.06693 -0.06585,-0.09226 -0.01835,-0.02532 -0.03059,-0.04143 -0.03671,-0.04831 l 0.276739,-0.183468 c 0.0047,0.0047 0.01548,0.01864 0.03239,0.04179 0.01692,0.02316 0.03635,0.05011 0.0583,0.08086 0.02232,0.02895 0.04481,0.05934 0.06748,0.09118 0.02195,0.03076 0.04012,0.05698 0.05451,0.07869 l 0.0049,-0.0032 c -0.0078,-0.06827 -0.01013,-0.133102 -0.0071,-0.194489 0.003,-0.06138 0.01529,-0.119494 0.03674,-0.174333 0.02181,-0.05664 0.05373,-0.109825 0.09575,-0.159556 0.04238,-0.05153 0.0983,-0.100321 0.167761,-0.14637 0.133486,-0.08849 0.256382,-0.131695 0.36869,-0.129604 0.113392,0.0014 0.223774,0.05005 0.331146,0.146026 l 0.0049,-0.0032 c -0.0078,-0.06827 -0.009,-0.133821 -0.0038,-0.196648 0.0052,-0.06282 0.02018,-0.122731 0.04488,-0.179729 0.02398,-0.05808 0.05807,-0.112702 0.10226,-0.163873 0.04347,-0.05225 0.09993,-0.1014 0.16939,-0.14745 0.08899,-0.05899 0.173414,-0.09778 0.253273,-0.116357 0.08094,-0.01929 0.158582,-0.01765 0.232919,0.0049 0.07433,0.02258 0.14753,0.06701 0.219588,0.133281 0.07134,0.06519 0.142978,0.152046 0.214928,0.260568 l 0.77812,1.173697 -0.289762,0.192102 -0.740347,-1.116722 c -0.05684,-0.08573 -0.109752,-0.153761 -0.158736,-0.204087 -0.04899,-0.05032 -0.09801,-0.08421 -0.147081,-0.101676 -0.0487,-0.01926 -0.09835,-0.02228 -0.148945,-0.009 -0.05131,0.01215 -0.106814,0.03801 -0.166501,0.07758 -0.06186,0.04101 -0.109836,0.09 -0.143929,0.146973 -0.03481,0.05589 -0.05502,0.118494 -0.06061,0.187815 -0.0045,0.06861 0.0061,0.143569 0.03193,0.224888 0.02688,0.0806 0.07017,0.165943 0.129893,0.256016 l 0.676672,1.020678 -0.289761,0.192101" + inkscape:connector-curvature="0" + id="path3887" /> + <path + d="m 34.448904,21.789158 -0.609488,-1.193191 c -0.04679,-0.0916 -0.09168,-0.165173 -0.134671,-0.220709 -0.04299,-0.05553 -0.08787,-0.09474 -0.134657,-0.117637 -0.04622,-0.02464 -0.09521,-0.03325 -0.14697,-0.02581 -0.05236,0.0063 -0.110422,0.0257 -0.174197,0.05828 -0.0661,0.03376 -0.119002,0.0776 -0.158718,0.131517 -0.04031,0.05276 -0.06686,0.113841 -0.07966,0.183247 -0.01223,0.06766 -0.01012,0.143342 0.0063,0.227054 0.01701,0.08196 0.0495,0.169907 0.09748,0.263831 l 0.557069,1.09057 -0.311343,0.159035 -0.756085,-1.480183 c -0.02014,-0.03942 -0.04174,-0.08029 -0.06482,-0.122605 -0.02251,-0.04406 -0.04411,-0.08493 -0.06482,-0.122605 -0.0213,-0.03883 -0.03963,-0.07187 -0.05501,-0.09911 -0.01537,-0.02723 -0.02572,-0.04462 -0.03102,-0.05214 l 0.295689,-0.15104 c 0.0041,0.0052 0.01328,0.02027 0.02747,0.04519 0.01419,0.02492 0.03045,0.0539 0.04879,0.08693 0.0189,0.03129 0.03782,0.06402 0.05675,0.09822 0.01834,0.03304 0.03343,0.06115 0.04527,0.08434 l 0.0052,-0.0027 c -9e-6,-0.06871 0.005,-0.133394 0.01493,-0.194045 0.01,-0.06065 0.02869,-0.117002 0.0562,-0.169068 0.02807,-0.05381 0.06579,-0.103054 0.113156,-0.147721 0.04793,-0.04642 0.109007,-0.08858 0.183219,-0.126487 0.142627,-0.07285 0.269616,-0.101897 0.380968,-0.08713 0.112512,0.01417 0.21669,0.07501 0.312536,0.182492 l 0.0052,-0.0027 c -1e-5,-0.06871 0.0061,-0.133986 0.01841,-0.195821 0.01228,-0.06183 0.03391,-0.119667 0.06489,-0.173511 0.03039,-0.055 0.07043,-0.105423 0.120114,-0.151275 0.04909,-0.04701 0.110745,-0.08947 0.184959,-0.127376 0.09508,-0.04857 0.183347,-0.07757 0.264794,-0.08701 0.0826,-0.01002 0.15956,3.77e-4 0.230871,0.03121 0.07131,0.03083 0.139019,0.08324 0.203131,0.157228 0.06352,0.07283 0.124892,0.167221 0.184125,0.283175 l 0.640584,1.254068 -0.309603,0.158147 -0.609489,-1.193191 c -0.04679,-0.0916 -0.09168,-0.165173 -0.134671,-0.220709 -0.04299,-0.05553 -0.08788,-0.09474 -0.134657,-0.117636 -0.04622,-0.02464 -0.09521,-0.03325 -0.14697,-0.02581 -0.05236,0.0063 -0.110423,0.0257 -0.174197,0.05828 -0.0661,0.03376 -0.119299,0.07702 -0.159607,0.129777 -0.0409,0.0516 -0.06805,0.111522 -0.08143,0.179768 -0.01223,0.06766 -0.01012,0.143343 0.0063,0.227055 0.0176,0.08312 0.05098,0.172806 0.100147,0.269048 l 0.557069,1.09057 -0.309604,0.158147" + inkscape:connector-curvature="0" + id="path3889" /> + <path + d="m 42.744275,18.955546 -0.04478,-0.295454 0.693255,-0.105066 -0.317246,-2.093284 -0.547647,0.531421 -0.04975,-0.328283 0.576028,-0.539672 0.320559,-0.04858 0.367584,2.425428 0.662358,-0.100383 0.04478,0.295454 -1.705138,0.258421" + inkscape:connector-curvature="0" + id="path3891" /> + <path + d="m 44.849923,16.947576 -0.03792,-0.402515 0.379181,-0.03572 0.03792,0.402515 -0.379181,0.03572 m 0.16027,1.701453 -0.03791,-0.402515 0.379181,-0.03572 0.03791,0.402515 -0.379181,0.03572" + inkscape:connector-curvature="0" + id="path3893" /> + <path + d="m 46.054835,18.552163 -0.01133,-0.298614 0.700668,-0.02658 -0.08025,-2.115666 -0.603843,0.466582 -0.01259,-0.331793 0.632971,-0.471596 0.323985,-0.01229 0.09298,2.451363 0.66944,-0.02539 0.01133,0.298613 -1.72337,0.06537" + inkscape:connector-curvature="0" + id="path3895" /> + <path + d="m 48.342118,18.501344 0.0076,-0.427668 0.3808,0.0067 -0.0076,0.427668 -0.380799,-0.0067" + inkscape:connector-curvature="0" + id="path3897" /> + <path + d="m 50.84888,18.003867 -0.04578,0.621362 -0.331133,-0.0244 0.04578,-0.621362 -1.293369,-0.09529 0.02009,-0.272698 1.392697,-1.757888 0.368143,0.02712 -0.13605,1.846558 0.385673,0.02841 -0.02038,0.276594 -0.385673,-0.02841 M 50.64504,16.251727 c -0.0031,0.0063 -0.01053,0.01881 -0.02235,0.03752 -0.01173,0.01742 -0.02495,0.03733 -0.03965,0.05975 -0.01471,0.02242 -0.03011,0.04544 -0.04621,0.06906 -0.01471,0.02242 -0.02783,0.04104 -0.03937,0.05585 l -0.779521,0.984446 c -0.0073,0.0099 -0.018,0.02283 -0.03223,0.03875 -0.01293,0.01602 -0.02656,0.03264 -0.04088,0.04987 -0.01423,0.01592 -0.02846,0.03185 -0.04269,0.04777 -0.01423,0.01593 -0.02557,0.02815 -0.03404,0.03666 l 0.970027,0.07147 0.106917,-1.451144" + inkscape:connector-curvature="0" + id="path3899" /> + <path + d="m 77.021288,37.280268 c -0.21155,0.08812 -0.411725,0.134119 -0.600525,0.138007 -0.187603,0.0034 -0.359123,-0.02744 -0.514564,-0.09245 -0.154241,-0.06552 -0.291149,-0.162243 -0.410726,-0.290158 -0.117876,-0.127218 -0.213112,-0.277969 -0.285709,-0.452254 l -0.395782,-0.950163 2.540378,-1.058172 0.34997,0.840182 c 0.08161,0.195922 0.132067,0.386482 0.151381,0.571681 0.02101,0.185899 0.0052,0.359625 -0.04738,0.521179 -0.0514,0.161051 -0.141462,0.307175 -0.270179,0.438372 -0.128222,0.132397 -0.30051,0.243656 -0.516863,0.333779 m -0.144194,-0.346169 c 0.171881,-0.0716 0.308258,-0.15732 0.409132,-0.257168 0.102072,-0.100351 0.173648,-0.211271 0.21473,-0.332759 0.04107,-0.121491 0.05386,-0.251648 0.03835,-0.390471 -0.01431,-0.139325 -0.05226,-0.282909 -0.113836,-0.430751 l -0.203524,-0.488604 -1.98867,0.828363 0.235817,0.566131 c 0.05557,0.133418 0.128528,0.247609 0.218863,0.342571 0.09083,0.09616 0.196195,0.167939 0.316083,0.215329 0.119885,0.04739 0.253296,0.06798 0.400234,0.06179 0.146934,-0.0062 0.30454,-0.04434 0.472818,-0.114432" + inkscape:connector-curvature="0" + id="path3901" /> + <path + d="m 76.885764,38.221869 c -0.105493,0.03348 -0.199569,0.07495 -0.28223,0.124408 -0.08103,0.05031 -0.146718,0.108039 -0.197074,0.1732 -0.04872,0.06601 -0.07985,0.140094 -0.09339,0.222259 -0.0123,0.08177 -0.0027,0.172297 0.02882,0.271585 0.04608,0.145204 0.112078,0.251989 0.197982,0.320352 0.0863,0.0696 0.179046,0.105738 0.278252,0.108406 l 0.0096,0.320723 c -0.06255,-0.0034 -0.128468,-0.01524 -0.197767,-0.03559 -0.06767,-0.01951 -0.134502,-0.05362 -0.20051,-0.102342 -0.06477,-0.04912 -0.127209,-0.116729 -0.187324,-0.202837 -0.05848,-0.08526 -0.108991,-0.194913 -0.15153,-0.328948 -0.09453,-0.297859 -0.07788,-0.553139 0.04997,-0.76584 0.128238,-0.211461 0.367348,-0.37273 0.717332,-0.483807 0.188643,-0.05987 0.355605,-0.0869 0.500887,-0.0811 0.146521,0.0054 0.273735,0.03607 0.381645,0.09198 0.107906,0.05591 0.197186,0.132768 0.267842,0.23056 0.07189,0.0974 0.127534,0.208151 0.166925,0.332259 0.05357,0.168785 0.07097,0.318313 0.05221,0.448583 -0.01837,0.131509 -0.06513,0.246755 -0.140265,0.345739 -0.07351,0.09983 -0.170886,0.184695 -0.292138,0.2546 -0.120861,0.07114 -0.256997,0.130742 -0.408406,0.178798 l -0.04468,0.01418 -0.456124,-1.437167 m 0.603131,1.009371 c 0.206314,-0.08597 0.343694,-0.188998 0.41214,-0.309078 0.06968,-0.120476 0.07794,-0.264486 0.02477,-0.43203 -0.01773,-0.05585 -0.04574,-0.111846 -0.08405,-0.167991 -0.03668,-0.0553 -0.08625,-0.101724 -0.148708,-0.139275 -0.06247,-0.03755 -0.139122,-0.06172 -0.229972,-0.0725 -0.08961,-0.01118 -0.196018,5.4e-5 -0.319218,0.03369 l 0.345047,1.087183" + inkscape:connector-curvature="0" + id="path3903" /> + <path + d="m 77.456336,41.797791 c -0.09624,0.02305 -0.186892,0.02469 -0.271967,0.0049 -0.08351,-0.01882 -0.159712,-0.05747 -0.228616,-0.115944 -0.06764,-0.05878 -0.127824,-0.136745 -0.180556,-0.233902 -0.05116,-0.0962 -0.09267,-0.210774 -0.124523,-0.343729 -0.02852,-0.119029 -0.0448,-0.228935 -0.04887,-0.32972 -0.005,-0.09922 0.0041,-0.189766 0.02731,-0.27165 0.02323,-0.08188 0.06185,-0.155405 0.115865,-0.220563 0.05558,-0.0642 0.128763,-0.119217 0.219538,-0.165065 l 0.131231,0.287896 c -0.103187,0.05551 -0.169818,0.135077 -0.199892,0.238685 -0.02881,0.103304 -0.0238,0.235996 0.01503,0.398077 0.0176,0.07344 0.03874,0.13934 0.06343,0.197694 0.02499,0.05962 0.05483,0.108706 0.08951,0.147262 0.03594,0.03825 0.07706,0.06455 0.12334,0.0789 0.04785,0.01531 0.102169,0.01569 0.16295,0.0011 0.06205,-0.01487 0.109207,-0.04156 0.141485,-0.08009 0.03354,-0.03883 0.05739,-0.08739 0.07155,-0.145677 0.01415,-0.05829 0.02178,-0.127061 0.02287,-0.206319 0.0027,-0.0783 0.0054,-0.164646 0.0082,-0.259047 0.0018,-0.08746 0.0063,-0.174901 0.01348,-0.262311 0.0072,-0.08741 0.0244,-0.169196 0.05168,-0.245355 0.02885,-0.0752 0.07071,-0.14079 0.125578,-0.196781 0.05487,-0.05599 0.131057,-0.09567 0.22856,-0.119025 0.187403,-0.0449 0.345783,-0.01255 0.475139,0.09705 0.130921,0.110561 0.227022,0.293733 0.288302,0.549516 0.0543,0.226659 0.05749,0.416022 0.0096,0.568091 -0.04762,0.153333 -0.155187,0.268811 -0.3227,0.346434 l -0.111703,-0.298599 c 0.05045,-0.02414 0.08976,-0.05565 0.11794,-0.09453 0.02944,-0.03919 0.04976,-0.08289 0.06098,-0.131095 0.01248,-0.04851 0.01663,-0.101057 0.01245,-0.157628 -0.0026,-0.05561 -0.0109,-0.112539 -0.02485,-0.170786 -0.03701,-0.154483 -0.08774,-0.262834 -0.152183,-0.325052 -0.06445,-0.06222 -0.144155,-0.08195 -0.239122,-0.0592 -0.05572,0.01335 -0.09875,0.03771 -0.129099,0.0731 -0.02878,0.03635 -0.05007,0.08162 -0.06387,0.135805 -0.01223,0.05515 -0.01965,0.119185 -0.02226,0.192113 -0.0013,0.07262 -0.0023,0.152516 -0.0028,0.239677 -8.86e-4,0.05779 -0.0027,0.117141 -0.0055,0.178066 -0.0015,0.06062 -0.0073,0.120253 -0.01739,0.178897 -0.0085,0.0596 -0.02231,0.116474 -0.04147,0.170607 -0.0176,0.05509 -0.04304,0.105374 -0.07633,0.150841 -0.03329,0.04546 -0.07537,0.085 -0.12623,0.11861 -0.05056,0.03487 -0.11256,0.06111 -0.186001,0.0787" + inkscape:connector-curvature="0" + id="path3905" /> + <path + d="m 79.476583,41.866986 0.330152,-0.06208 0.06496,0.345508 -0.330152,0.06208 -0.06496,-0.345509 m -2.518371,0.473514 2.076888,-0.390504 0.06496,0.345508 -2.076888,0.390504 -0.06496,-0.345508" + inkscape:connector-curvature="0" + id="path3907" /> + <path + d="m 76.409909,44.121315 c -0.01605,-0.120027 -0.01802,-0.228141 -0.0059,-0.324341 0.0121,-0.0962 0.03698,-0.18032 0.07464,-0.252352 0.03637,-0.07186 0.08342,-0.132668 0.141132,-0.182422 0.05772,-0.04976 0.124086,-0.08884 0.199106,-0.117265 l 0.09718,0.343668 c -0.09369,0.03617 -0.161082,0.09707 -0.20218,0.182702 -0.04221,0.08709 -0.05469,0.195167 -0.03744,0.324228 0.01053,0.07872 0.03107,0.148887 0.06164,0.210484 0.03057,0.0616 0.07339,0.1117 0.128476,0.150315 0.05379,0.03878 0.121045,0.06526 0.20176,0.07943 0.08071,0.01417 0.176567,0.01383 0.287559,-10e-4 l 0.336846,-0.04504 -5.18e-4,-0.0039 c -0.05637,-0.01874 -0.111659,-0.04419 -0.165878,-0.07635 -0.05276,-0.03104 -0.101558,-0.07181 -0.146406,-0.122301 -0.04467,-0.0492 -0.08346,-0.108386 -0.116353,-0.177552 -0.0316,-0.06934 -0.05362,-0.150473 -0.06604,-0.243395 -0.01777,-0.132932 -0.01111,-0.250084 0.02001,-0.351454 0.03257,-0.100254 0.08933,-0.186663 0.170272,-0.259228 0.08111,-0.07127 0.185592,-0.129908 0.313437,-0.175902 0.129307,-0.04488 0.281077,-0.07896 0.455308,-0.102258 0.167777,-0.02243 0.320589,-0.02907 0.458436,-0.01991 0.138017,0.01045 0.257629,0.04043 0.358837,0.08996 0.102667,0.05064 0.185166,0.122372 0.247498,0.215191 0.06379,0.09394 0.105265,0.212533 0.124421,0.35579 0.01984,0.148418 0.0028,0.281405 -0.05109,0.398962 -0.05245,0.118673 -0.137292,0.216063 -0.254541,0.29217 l 5.18e-4,0.0039 c 0.03226,-0.0043 0.06849,-0.0085 0.108669,-0.01256 0.04035,-0.0028 0.07804,-0.0058 0.113059,-0.0092 0.03648,-0.0023 0.0678,-0.0038 0.09396,-0.0047 0.02616,-8.71e-4 0.04134,-2.75e-4 0.04556,0.0018 l 0.04426,0.331039 c -0.01179,2.6e-4 -0.03197,0.0016 -0.06053,0.0042 -0.02728,0.0023 -0.06036,0.0054 -0.09925,0.0093 -0.03889,0.0039 -0.0823,0.0084 -0.130223,0.01347 -0.04646,0.0062 -0.09486,0.01268 -0.145192,0.01941 l -1.600987,0.214061 c -0.292966,0.03917 -0.522676,-0.0037 -0.689131,-0.128556 -0.167573,-0.123414 -0.271204,-0.333538 -0.310893,-0.630374 m 1.940999,0.280395 c 0.145836,-0.0195 0.268222,-0.05229 0.367157,-0.09835 0.100395,-0.04495 0.180053,-0.09764 0.238973,-0.158065 0.05892,-0.06042 0.09938,-0.126264 0.121379,-0.197515 0.02217,-0.06996 0.0286,-0.13979 0.01928,-0.209482 -0.01191,-0.08905 -0.03766,-0.163771 -0.07726,-0.224159 -0.03943,-0.0591 -0.09555,-0.105456 -0.168353,-0.139072 -0.07134,-0.0325 -0.160665,-0.05208 -0.267963,-0.05876 -0.105837,-0.0056 -0.231029,0.0013 -0.375575,0.02066 -0.151,0.02019 -0.27683,0.04687 -0.377489,0.08003 -0.0992,0.03428 -0.177653,0.0763 -0.23537,0.126055 -0.05772,0.04975 -0.0959,0.108062 -0.114547,0.174926 -0.01865,0.06686 -0.0221,0.144174 -0.01037,0.231936 0.0093,0.06969 0.03322,0.135463 0.0717,0.197316 0.03866,0.06314 0.09379,0.116855 0.165392,0.161143 0.0716,0.04428 0.160542,0.07574 0.266814,0.09438 0.106271,0.01863 0.23168,0.01828 0.376228,-0.001" + inkscape:connector-curvature="0" + id="path3909" /> + <path + d="m 77.478883,46.76703 1.337606,-0.07741 c 0.102691,-0.0059 0.188034,-0.01871 0.256028,-0.03829 0.06806,-0.01829 0.12192,-0.04553 0.161563,-0.08174 0.04094,-0.03628 0.06824,-0.08286 0.08191,-0.139731 0.01504,-0.05565 0.02019,-0.124422 0.01545,-0.206315 -0.0048,-0.0832 -0.02413,-0.157725 -0.05794,-0.223589 -0.03244,-0.06464 -0.07799,-0.119392 -0.136671,-0.164253 -0.05731,-0.04364 -0.127658,-0.07609 -0.211055,-0.09734 -0.0821,-0.02134 -0.175796,-0.02895 -0.281087,-0.02286 l -1.222564,0.07075 -0.02031,-0.350976 1.659333,-0.09603 c 0.0442,-0.0026 0.0903,-0.0059 0.138327,-0.01 0.0494,-0.0029 0.0955,-0.0062 0.138327,-0.01 0.04412,-0.0039 0.08174,-0.0073 0.112867,-0.01044 0.03112,-0.0031 0.05119,-0.0056 0.06022,-0.0074 l 0.01918,0.331477 c -0.0064,0.0017 -0.0239,0.004 -0.05242,0.0069 -0.02852,0.003 -0.0616,0.0062 -0.09922,0.0097 -0.03625,0.0047 -0.07383,0.0088 -0.112753,0.01239 -0.03762,0.0035 -0.06943,0.006 -0.09543,0.0075 l 3.39e-4,0.0058 c 0.06305,0.03026 0.12044,0.06411 0.17217,0.101551 0.05173,0.03744 0.09603,0.08183 0.132916,0.133168 0.03818,0.05126 0.06836,0.110818 0.09055,0.178661 0.02348,0.06777 0.03786,0.147145 0.04313,0.23814 0.0068,0.116991 -0.0017,0.218561 -0.02541,0.304711 -0.02371,0.08615 -0.06388,0.158251 -0.120517,0.216309 -0.05664,0.05806 -0.131688,0.102178 -0.22515,0.13237 -0.09209,0.03141 -0.20313,0.05088 -0.333119,0.05841 l -1.405851,0.08136 -0.02043,-0.352926" + inkscape:connector-curvature="0" + id="path3911" /> + <path + d="m 78.487682,47.938431 c -0.110662,-0.0018 -0.213045,0.0076 -0.307147,0.02813 -0.09282,0.02191 -0.17347,0.05574 -0.241943,0.101487 -0.06719,0.04707 -0.12028,0.107402 -0.159261,0.180988 -0.03768,0.07361 -0.05738,0.162485 -0.0591,0.266639 -0.0025,0.152321 0.02607,0.274555 0.08575,0.3667 0.05966,0.09345 0.136094,0.157216 0.229294,0.191313 l -0.09298,0.3071 c -0.05822,-0.0231 -0.116941,-0.05533 -0.176161,-0.09667 -0.05794,-0.04003 -0.110448,-0.09364 -0.157522,-0.160827 -0.04577,-0.06717 -0.08345,-0.15114 -0.113043,-0.251901 -0.02831,-0.09944 -0.0413,-0.219463 -0.03898,-0.360068 0.0052,-0.312458 0.102183,-0.549169 0.291065,-0.710134 0.18886,-0.159663 0.466859,-0.236462 0.833997,-0.230398 0.197888,0.0033 0.364774,0.03077 0.500659,0.0825 0.137183,0.05175 0.248028,0.1213 0.332537,0.208645 0.0845,0.08734 0.144689,0.188613 0.180554,0.303805 0.03716,0.115212 0.05467,0.237914 0.05252,0.368105 -0.0029,0.177058 -0.03401,0.324351 -0.09325,0.44188 -0.05926,0.118827 -0.140261,0.213205 -0.242991,0.283135 -0.101452,0.07125 -0.220775,0.120717 -0.357968,0.148405 -0.137218,0.02898 -0.285242,0.04217 -0.444073,0.03955 l -0.04687,-7.74e-4 0.0249,-1.507607 m 0.250592,1.148827 c 0.222947,-0.01585 0.38597,-0.06981 0.489069,-0.161867 0.104397,-0.09204 0.158048,-0.225938 0.160953,-0.401694 9.65e-4,-0.05859 -0.0078,-0.120588 -0.02623,-0.186005 -0.01717,-0.06409 -0.04939,-0.12388 -0.09666,-0.179355 -0.04727,-0.05548 -0.112252,-0.10278 -0.194949,-0.141911 -0.0814,-0.03911 -0.185846,-0.06232 -0.313346,-0.06964 l -0.01884,1.14047" + inkscape:connector-curvature="0" + id="path3913" /> + <path + d="m 77.670156,51.238318 c -0.128928,-0.07716 -0.217647,-0.172223 -0.266159,-0.285179 -0.04863,-0.111663 -0.06572,-0.246591 -0.05125,-0.404785 0.0243,-0.265819 0.131669,-0.453438 0.322108,-0.562857 0.19032,-0.108124 0.467015,-0.14559 0.830085,-0.1124 0.733918,0.06709 1.077288,0.358674 1.030113,0.874752 -0.01458,0.15949 -0.05591,0.289732 -0.123975,0.390727 -0.06807,0.100992 -0.168607,0.178097 -0.301607,0.231316 l -3.55e-4,0.0039 c 0.01297,0.0012 0.03312,0.0024 0.06047,0.0036 0.02853,0.0026 0.05776,0.0046 0.0877,0.0061 0.03112,0.0028 0.06029,0.0055 0.08753,0.008 0.02723,0.0025 0.04668,0.0043 0.05835,0.0053 l 0.814961,0.0745 -0.032,0.350103 -2.452664,-0.224209 c -0.05057,-0.0046 -0.0992,-0.0091 -0.145876,-0.01333 -0.0468,-0.003 -0.08971,-0.0056 -0.128726,-0.0078 -0.03902,-0.0023 -0.07285,-0.004 -0.101497,-0.0054 -0.02735,-0.0012 -0.04692,-0.0017 -0.05871,-0.0014 l 0.03058,-0.334542 c 0.0132,-0.0014 0.03159,-0.0023 0.05517,-0.0028 0.02475,9.54e-4 0.05281,0.0016 0.08417,0.0018 0.03124,0.0015 0.06377,0.0032 0.09761,0.005 0.03513,0.0019 0.06955,0.0044 0.103264,0.0075 l 7.11e-4,-0.0078 m 0.812846,-0.996547 c -0.145229,-0.01328 -0.270539,-0.01558 -0.375932,-0.0069 -0.105395,0.0087 -0.193642,0.03002 -0.264743,0.06406 -0.06981,0.03415 -0.123113,0.08093 -0.159923,0.140321 -0.03681,0.0594 -0.0593,0.133828 -0.06748,0.2233 -0.0084,0.09206 -9.13e-4,0.174468 0.02251,0.247216 0.0233,0.07404 0.06589,0.137427 0.127751,0.190154 0.06304,0.05414 0.147248,0.09845 0.25262,0.132924 0.105371,0.03447 0.235858,0.05882 0.39146,0.07305 0.149117,0.01363 0.27602,0.01281 0.380711,-0.0025 0.105985,-0.01516 0.192939,-0.04382 0.260864,-0.08599 0.06792,-0.04217 0.118698,-0.09702 0.152331,-0.16455 0.03481,-0.06612 0.05636,-0.144559 0.06466,-0.235325 0.0078,-0.08558 -3.34e-4,-0.160856 -0.02447,-0.225821 -0.02414,-0.06497 -0.06737,-0.121221 -0.129707,-0.168759 -0.06234,-0.04754 -0.145723,-0.08654 -0.250152,-0.117008 -0.103254,-0.02905 -0.230088,-0.05045 -0.380501,-0.0642" + inkscape:connector-curvature="0" + id="path3915" /> + <path + d="m 78.791939,53.959052 -1.825621,-0.342233 0.06478,-0.345543 1.825621,0.342232 0.0547,-0.291792 0.251479,0.04714 -0.0547,0.291792 0.234202,0.0439 c 0.07551,0.01415 0.143934,0.03427 0.205287,0.06034 0.06263,0.02631 0.11414,0.06246 0.154538,0.108456 0.04143,0.04751 0.06947,0.106418 0.08411,0.176727 0.01592,0.07055 0.01416,0.157651 -0.0053,0.261315 -0.0077,0.04095 -0.01752,0.08282 -0.02951,0.12562 -0.01224,0.04407 -0.02532,0.08203 -0.03923,0.113862 l -0.262997,-0.0493 c 0.0079,-0.02104 0.01591,-0.04603 0.02399,-0.07499 0.0091,-0.02744 0.01583,-0.05267 0.02015,-0.07571 0.0096,-0.05119 0.01047,-0.09475 0.0026,-0.130657 -0.0081,-0.03464 -0.02376,-0.06407 -0.04704,-0.0883 -0.02224,-0.02272 -0.05183,-0.04151 -0.08879,-0.05639 -0.0372,-0.0136 -0.08075,-0.02507 -0.130659,-0.03443 l -0.18621,-0.03491 -0.07593,0.405054 -0.251478,-0.04714 0.07593,-0.405054" + inkscape:connector-curvature="0" + id="path3917" /> + <path + d="m 77.441728,56.26852 c -0.358999,-0.08869 -0.606835,-0.233741 -0.743508,-0.435155 -0.136672,-0.201419 -0.167847,-0.452553 -0.09353,-0.753403 0.03529,-0.142842 0.08832,-0.265206 0.159089,-0.367091 0.07077,-0.101886 0.160081,-0.181086 0.267928,-0.237599 0.107534,-0.05525 0.232818,-0.08734 0.375852,-0.09626 0.143985,-0.0073 0.30636,0.0113 0.487124,0.05596 0.707885,0.174875 0.9861,0.568852 0.834647,1.181933 -0.03935,0.159273 -0.09535,0.290961 -0.168011,0.395064 -0.07297,0.105364 -0.162759,0.183776 -0.269355,0.235235 -0.106911,0.05272 -0.231103,0.08038 -0.372574,0.08299 -0.141473,0.0026 -0.300695,-0.01795 -0.477666,-0.06167 m 0.08853,-0.358367 c 0.159274,0.03934 0.293861,0.05918 0.403762,0.05951 0.109587,0.0016 0.201284,-0.01399 0.275092,-0.04672 0.07349,-0.03147 0.13068,-0.07837 0.171562,-0.140695 0.04214,-0.06202 0.07383,-0.136003 0.09507,-0.221959 0.02155,-0.08722 0.02779,-0.169507 0.01873,-0.246852 -0.0081,-0.07577 -0.03767,-0.146113 -0.0887,-0.211025 -0.04976,-0.0646 -0.123192,-0.122978 -0.220288,-0.17513 -0.09741,-0.05089 -0.223855,-0.09554 -0.379336,-0.133948 -0.159276,-0.03935 -0.294807,-0.05808 -0.406595,-0.05618 -0.110526,0.0022 -0.202691,0.01967 -0.276498,0.05241 -0.07412,0.034 -0.131938,0.08074 -0.173458,0.140226 -0.04183,0.06075 -0.07243,0.130312 -0.09179,0.208686 -0.02155,0.08722 -0.02906,0.169192 -0.02253,0.245915 0.0078,0.07703 0.03657,0.14785 0.08633,0.212453 0.04976,0.0646 0.123822,0.123132 0.222183,0.175598 0.09836,0.05246 0.227178,0.09837 0.386453,0.137718" + inkscape:connector-curvature="0" + id="path3919" /> + <path + d="m 76.277382,56.441011 1.547231,0.483758 c 0.04225,0.01321 0.08532,0.02599 0.129208,0.03835 0.04474,0.01399 0.08781,0.02677 0.129208,0.03835 0.0414,0.01158 0.08031,0.02238 0.116742,0.03241 0.03643,0.01002 0.06913,0.01888 0.0981,0.02658 l -0.09908,0.316902 c -0.02897,-0.0077 -0.0623,-0.01675 -0.09997,-0.02716 -0.03682,-0.0088 -0.07468,-0.01858 -0.113597,-0.02938 -0.0393,-0.0096 -0.07697,-0.01998 -0.113013,-0.03124 -0.03519,-0.0096 -0.06521,-0.01834 -0.09006,-0.02611 l -0.0023,0.0075 c 0.06967,0.0477 0.128383,0.09334 0.176149,0.13693 0.04862,0.04521 0.08531,0.09147 0.110085,0.138783 0.02601,0.0477 0.03995,0.09912 0.04181,0.154271 0.0031,0.05554 -0.0064,0.118724 -0.02857,0.189563 -0.0085,0.02734 -0.01919,0.05266 -0.03194,0.07596 -0.0115,0.02369 -0.02125,0.04178 -0.02925,0.05429 l -0.307582,-0.09617 c 0.01321,-0.02043 0.02572,-0.04517 0.03753,-0.07421 0.01305,-0.02866 0.02541,-0.06163 0.03707,-0.09891 0.02409,-0.07705 0.02728,-0.148358 0.0096,-0.213917 -0.01809,-0.06432 -0.05276,-0.123591 -0.104021,-0.177815 -0.0504,-0.0526 -0.115956,-0.100376 -0.196654,-0.143341 -0.07946,-0.04258 -0.169517,-0.0796 -0.270179,-0.111077 l -1.051371,-0.328722 0.104911,-0.335544" + inkscape:connector-curvature="0" + id="path3921" /> + <path + d="m 77.57706,60.103407 -0.941955,-0.400182 -0.600272,1.412932 -0.284025,-0.120665 0.600273,-1.412932 -1.026443,-0.436076 0.145868,-0.343346 2.532851,1.076061 -0.764469,1.799421 -0.28043,-0.119138 0.618602,-1.456075" + inkscape:connector-curvature="0" + id="path3923" /> + <path + d="m 73.672921,62.572089 0.858474,0.09708 0.58473,-1.109222 -0.564288,-0.655144 0.180337,-0.342096 1.910706,2.27677 -0.197642,0.374923 -2.949922,-0.305396 0.177605,-0.336913 m 2.136088,0.245098 c 0.05579,0.0059 0.111848,0.01259 0.168183,0.02021 0.05572,0.0088 0.105932,0.01684 0.150624,0.02421 0.04584,0.008 0.08386,0.01403 0.114059,0.01818 0.03074,0.0059 0.0493,0.0098 0.05566,0.01168 -0.0045,-0.0053 -0.01794,-0.01903 -0.04018,-0.04105 -0.02224,-0.02203 -0.0493,-0.05028 -0.08118,-0.08474 -0.03248,-0.03332 -0.06779,-0.07106 -0.105902,-0.113231 -0.03873,-0.04102 -0.07657,-0.08232 -0.113535,-0.123879 l -0.63601,-0.732695 -0.479988,0.91053 0.968262,0.110794" + inkscape:connector-curvature="0" + id="path3925" /> + <path + d="m 73.310171,63.196101 2.308254,1.498402 -1.136829,1.751259 -0.255563,-0.165899 0.93371,-1.438358 -0.740476,-0.48068 -0.869903,1.340065 -0.252286,-0.163771 0.869903,-1.340065 -0.804367,-0.522155 -0.977311,1.505526 -0.255563,-0.165899 1.180431,-1.818425" + inkscape:connector-curvature="0" + id="path3927" /> + <path + d="m 70.724956,66.823317 2.751648,0.30052 c -0.05164,-0.03757 -0.103165,-0.07422 -0.154586,-0.109958 -0.04347,-0.0311 -0.08909,-0.06473 -0.13685,-0.100896 -0.04755,-0.03434 -0.08919,-0.06565 -0.124918,-0.09394 l -1.433179,-1.13492 0.206129,-0.260299 2.157423,1.708441 -0.26918,0.339921 -2.773597,-0.297971 c 0.05244,0.03655 0.104887,0.07309 0.15733,0.10964 0.04449,0.03191 0.09255,0.06664 0.144187,0.104215 0.05266,0.03837 0.101951,0.07575 0.147886,0.112127 l 1.414804,1.120369 -0.208553,0.263361 -2.157424,-1.708441 0.27888,-0.35217" + inkscape:connector-curvature="0" + id="path3929" /> + <path + d="m 68.636859,69.108388 1.509272,-1.588823 0.202497,0.192358 0.417682,2.708113 1.055952,-1.11161 0.220906,0.209845 -1.347853,1.418896 -0.196833,-0.186978 -0.423345,-2.713493 -1.217372,1.281538 -0.220906,-0.209846" + inkscape:connector-curvature="0" + id="path3931" /> + <path + d="m 66.845397,70.716316 0.76893,0.39389 0.938663,-0.831379 -0.296674,-0.81217 0.289494,-0.256407 0.983929,2.804704 -0.317274,0.281012 -2.652176,-1.327128 0.285108,-0.252522 m 1.912028,0.983409 c 0.05013,0.02517 0.100201,0.05127 0.150218,0.07828 0.04904,0.02788 0.09317,0.05315 0.132381,0.07582 0.04008,0.02364 0.07351,0.04273 0.100299,0.05727 0.02668,0.01637 0.04266,0.02657 0.04796,0.03058 -0.0024,-0.0066 -0.01007,-0.02413 -0.0231,-0.05259 -0.01303,-0.02846 -0.02838,-0.06445 -0.04603,-0.107944 -0.01863,-0.04264 -0.03833,-0.09041 -0.05911,-0.143327 -0.02175,-0.05205 -0.04258,-0.104047 -0.06249,-0.155984 l -0.336401,-0.910045 -0.770522,0.682456 0.866806,0.445481" + inkscape:connector-curvature="0" + id="path3933" /> + </g> + </g> + <g + id="layer4" + style="display:inline"> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 1 1 -27.274118,0 13.637059,13.637059 0 1 1 27.274118,0 z" + transform="matrix(1.0266143,0,0,1.0266143,-0.999999,0.7783622)" + id="path3757" + style="fill:#000000;fill-opacity:1;stroke:none" /> + <path + d="M 47.5,33 C 39.491871,33 33,39.491871 33,47.5 33,55.508129 39.491871,62 47.5,62 c 0.08504,0 0.165309,0.0015 0.25,0 C 40.133878,61.866031 34,55.648102 34,48 c 0,-7.731986 6.268014,-14 14,-14 7.648102,0 13.866031,6.133878 14,13.75 0.0015,-0.08469 0,-0.164961 0,-0.25 C 62,39.491871 55.508129,33 47.5,33 z" + inkscape:connector-curvature="0" + id="path3760" + style="opacity:0.6;fill:#000000;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 1 1 -27.274118,0 13.637059,13.637059 0 1 1 27.274118,0 z" + transform="matrix(1.0266143,0,0,1.0266143,-0.999999,0.778362)" + id="path2907" + style="fill:url(#radialGradient3687);fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 1 1 -27.274118,0 13.637059,13.637059 0 1 1 27.274118,0 z" + transform="matrix(0,0.97161711,-0.97161711,0,92.69191,1.6250011)" + id="path3705" + style="fill:none;stroke:url(#linearGradient3733);stroke-width:0.514606;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3774)" /> + <path + d="m 41.34375,47.375 c 0.07082,-0.767299 0.285421,-1.486362 0.59375,-2.15625 L 37.34375,42.8125 c -0.603997,1.243084 -1.018186,2.621977 -1.15625,4.0625 l 5.15625,0.5 z m 1.375,-3.4375 c 1.071033,-1.389071 2.667628,-2.34897 4.5,-2.5625 L 46.625,36.21875 c -3.598468,0.417087 -6.654118,2.435762 -8.53125,5.3125 l 4.625,2.40625 z" + inkscape:connector-curvature="0" + id="path3735" + style="opacity:0.6;fill:#ffffff;fill-opacity:1;stroke:none" /> + <path + d="m 50.708204,52 a 9.7082043,6 0 1 1 -19.416408,0 9.7082043,6 0 1 1 19.416408,0 z" + transform="matrix(0.58268801,-0.58268801,0.58268801,0.58268801,-0.74355667,46.935862)" + id="path3759" + style="opacity:0.4314516;fill:url(#linearGradient3767);fill-opacity:1;stroke:none" /> + <path + d="m 50.708204,52 a 9.7082043,6 0 1 1 -19.416408,0 9.7082043,6 0 1 1 19.416408,0 z" + transform="matrix(0.15076894,-0.15076894,0.15076894,0.15076894,37.387737,49.649792)" + id="path3769" + style="opacity:0.3;fill:url(#linearGradient3771);fill-opacity:1;stroke:none" /> + <path + d="m 50.708204,52 a 9.7082043,6 0 1 1 -19.416408,0 9.7082043,6 0 1 1 19.416408,0 z" + transform="matrix(-0.10757707,0.10757707,-0.10757707,-0.10757707,55.999137,47.076819)" + id="path3773" + style="opacity:0.3;fill:url(#linearGradient3775);fill-opacity:1;stroke:none" /> + <path + d="m 50.708204,52 a 9.7082043,6 0 1 1 -19.416408,0 9.7082043,6 0 1 1 19.416408,0 z" + transform="matrix(-0.19358861,0.19358861,-0.19358861,-0.19358861,59.194255,43.21899)" + id="path3777" + style="opacity:0.4314516;fill:url(#linearGradient3779);fill-opacity:1;stroke:none" /> + <path + d="m 50.708204,52 a 9.7082043,6 0 1 1 -19.416408,0 9.7082043,6 0 1 1 19.416408,0 z" + transform="matrix(0.08968638,-0.08968638,0.08968638,0.08968638,40.497472,47.750757)" + id="path3789" + style="opacity:0.6;fill:url(#linearGradient3791);fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 1 1 -27.274118,0 13.637059,13.637059 0 1 1 27.274118,0 z" + transform="matrix(1.0266143,0,0,1.0266143,69.000001,0.7783622)" + id="path3779" + style="opacity:0.6;fill:#ff00ff;fill-opacity:1;stroke:none;display:inline" /> + <path + d="m 48,32 c -8.836556,0 -16,7.163444 -16,16 0,8.836556 7.163444,16 16,16 8.836556,0 16,-7.163444 16,-16 0,-8.836556 -7.163444,-16 -16,-16 z m 0,1.5 c 8.008129,0 14.5,6.491871 14.5,14.5 0,8.008129 -6.491871,14.5 -14.5,14.5 -8.008129,0 -14.5,-6.491871 -14.5,-14.5 0,-8.008129 6.491871,-14.5 14.5,-14.5 z" + inkscape:connector-curvature="0" + id="path3806" + style="opacity:0.7;fill:url(#linearGradient3817);fill-opacity:1;stroke:none;display:inline" /> + </g> + <g + id="layer6" + style="display:inline"> + <g + clip-path="url(#clipPath3798)" + id="g3786" + style="opacity:0.4;display:inline;filter:url(#filter3833)"> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(1.0266143,0,0,1.0266143,-0.9999987,0.7783622)" + id="path3756" + style="fill:#d40000;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(0.72592593,0.72592593,-0.72592593,0.72592593,46.742509,-20.038972)" + id="path3772" + style="fill:#d42aff;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(0,1.0266143,-1.0266143,0,95.221638,-0.99999877)" + id="path3774" + style="fill:#6600ff;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(-0.72592593,0.72592593,-0.72592593,-0.72592593,116.03897,46.742509)" + id="path3776" + style="fill:#2a7fff;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(-1.0266143,0,0,-1.0266143,96.999997,95.221637)" + id="path3778" + style="fill:#5fd3bc;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(-0.72592593,-0.72592593,0.72592593,-0.72592593,49.25749,116.03897)" + id="path3780" + style="fill:#55d400;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(0,-1.0266143,1.0266143,0,0.77836254,96.999997)" + id="path3782" + style="fill:#ffcc00;fill-opacity:1;stroke:none" /> + <path + d="m 61.366765,45.997448 a 13.637059,13.637059 0 0 1 -3.994202,9.642857 l -9.642857,-9.642857 z" + transform="matrix(0.72592593,-0.72592593,0.72592593,0.72592593,-20.03897,49.25749)" + id="path3784" + style="fill:#ff6600;fill-opacity:1;stroke:none" /> + </g> + <g + transform="translate(0,-50)" + id="g3957" + style="opacity:0.8;filter:url(#filter3987)"> + <path + d="m 61.53125,94.375 -0.96875,0.25 C 60.849921,95.699342 61,96.835019 61,98 c 0,4.80784 -2.619633,9.00086 -6.5,11.25 l 0.5,0.875 c 5.487553,-3.16824 8.171252,-9.62943 6.53125,-15.75 z" + inkscape:connector-curvature="0" + id="path3726" + style="opacity:0.8;fill:url(#linearGradient3967);fill-opacity:1;stroke:none" /> + <path + d="m 59.25,104.5 c -2.249137,3.88037 -6.442164,6.5 -11.25,6.5 -1.164981,0 -2.300658,-0.15008 -3.375,-0.4375 l -0.25,0.96875 c 6.120569,1.64 12.58176,-1.0437 15.75,-6.53125 l -0.875,-0.5 z" + inkscape:connector-curvature="0" + id="path3728" + style="opacity:0.8;fill:url(#linearGradient3969);fill-opacity:1;stroke:none" /> + <path + d="m 36.75,104.5 -0.875,0.5 c 3.16824,5.48755 9.629431,8.17125 15.75,6.53125 l -0.25,-0.96875 C 50.300658,110.84992 49.164981,111 48,111 c -4.807836,0 -9.000863,-2.61963 -11.25,-6.5 z" + inkscape:connector-curvature="0" + id="path3730" + style="fill:url(#linearGradient3971);fill-opacity:1;stroke:none" /> + <path + d="m 34.46875,94.375 c -1.640002,6.12057 1.043697,12.58176 6.53125,15.75 l 0.5,-0.875 C 37.619633,107.00086 35,102.80784 35,98 c 0,-1.164981 0.150079,-2.300658 0.4375,-3.375 l -0.96875,-0.25 z" + inkscape:connector-curvature="0" + id="path3732" + style="opacity:0.9;fill:url(#linearGradient3973);fill-opacity:1;stroke:none" /> + <path + d="m 41,85.875 c -5.487553,3.16824 -8.171252,9.629431 -6.53125,15.75 l 0.96875,-0.25 C 35.150079,100.30066 35,99.164981 35,98 35,93.192164 37.619633,88.999137 41.5,86.75 L 41,85.875 z" + inkscape:connector-curvature="0" + id="path3734" + style="fill:url(#linearGradient3975);fill-opacity:1;stroke:none" /> + <path + d="M 48.15625,84 C 43.183834,83.93941 38.449195,86.541364 35.875,91 l 0.875,0.5 C 38.999137,87.619633 43.192164,85 48,85 c 1.164981,0 2.300658,0.150079 3.375,0.4375 l 0.25,-0.96875 C 50.477393,84.16125 49.303731,84.013982 48.15625,84 z" + inkscape:connector-curvature="0" + id="path3736" + style="fill:url(#linearGradient3977);fill-opacity:1;stroke:none" /> + <path + d="m 47.84375,84 c -1.147481,0.01398 -2.321143,0.16125 -3.46875,0.46875 l 0.25,0.96875 C 45.699342,85.150079 46.835019,85 48,85 c 4.807836,0 9.000863,2.619633 11.25,6.5 L 60.125,91 C 57.550805,86.541363 52.816166,83.939411 47.84375,84 z" + inkscape:connector-curvature="0" + id="path3738" + style="fill:url(#linearGradient3979);fill-opacity:1;stroke:none" /> + <path + d="m 55,85.875 -0.5,0.875 c 3.880367,2.249137 6.5,6.442164 6.5,11.25 0,1.164981 -0.150079,2.30066 -0.4375,3.375 l 0.96875,0.25 C 63.171252,95.504431 60.487553,89.04324 55,85.875 z" + inkscape:connector-curvature="0" + id="path3740" + style="opacity:0.9;fill:url(#linearGradient3981);fill-opacity:1;stroke:none" /> + </g> + <path + d="m 50.708204,52 a 9.7082043,6 0 1 1 -19.416408,0 9.7082043,6 0 1 1 19.416408,0 z" + transform="matrix(0.23287504,-0.23287504,0.23287504,0.23287504,33.266783,52.13527)" + id="path3793" + style="fill:url(#radialGradient3803);fill-opacity:1;stroke:none;display:inline" /> + </g> +</svg> diff --git a/rapid/glade3/photo24.png b/rapid/glade3/photo24.png Binary files differdeleted file mode 100644 index 53b2271..0000000 --- a/rapid/glade3/photo24.png +++ /dev/null diff --git a/rapid/glade3/photo66.png b/rapid/glade3/photo66.png Binary files differnew file mode 100644 index 0000000..1bef29f --- /dev/null +++ b/rapid/glade3/photo66.png diff --git a/rapid/glade3/photo_icon.png b/rapid/glade3/photo_icon.png Binary files differnew file mode 100644 index 0000000..52e22bc --- /dev/null +++ b/rapid/glade3/photo_icon.png diff --git a/rapid/glade3/photo_small.png b/rapid/glade3/photo_small.png Binary files differdeleted file mode 100644 index f44d380..0000000 --- a/rapid/glade3/photo_small.png +++ /dev/null diff --git a/rapid/glade3/photo_small_shadow.png b/rapid/glade3/photo_small_shadow.png Binary files differdeleted file mode 100644 index fe85cd9..0000000 --- a/rapid/glade3/photo_small_shadow.png +++ /dev/null diff --git a/rapid/glade3/rapid.glade b/rapid/glade3/prefs.ui index 163174b..ddbacdf 100644 --- a/rapid/glade3/rapid.glade +++ b/rapid/glade3/prefs.ui @@ -1,8 +1,19 @@ <?xml version="1.0" encoding="UTF-8"?> -<glade-interface> - <!-- interface-requires gtk+ 2.16 --> - <!-- interface-naming-policy toplevel-contextual --> - <widget class="GtkDialog" id="preferencesdialog"> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkAdjustment" id="hour_adjustment"> + <property name="upper">23</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="minute_adjustment"> + <property name="upper">59</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkDialog" id="preferencesdialog"> + <property name="can_focus">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="border_width">5</property> <property name="title" translatable="yes">Preferences: Rapid Photo Downloader</property> @@ -11,18 +22,64 @@ <property name="default_height">500</property> <property name="icon">rapid-photo-downloader.svg</property> <property name="type_hint">dialog</property> - <signal name="destroy" handler="on_preferencesdialog_destroy"/> - <signal name="response" handler="on_response"/> + <signal name="destroy" handler="on_preferencesdialog_destroy" swapped="no"/> + <signal name="response" handler="on_preferencesdialog_response" swapped="no"/> <child internal-child="vbox"> - <widget class="GtkVBox" id="dialog-vbox2"> + <object class="GtkVBox" id="dialog-vbox2"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="help_button1"> + <property name="label">gtk-help</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + <property name="secondary">True</property> + </packing> + </child> + <child> + <object class="GtkButton" id="close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> <child> - <widget class="GtkHBox" id="hbox3"> + <object class="GtkHBox" id="hbox7"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">2</property> <child> - <widget class="GtkScrolledWindow" id="scrolled_window"> + <object class="GtkScrolledWindow" id="scrolled_window"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="resize_mode">queue</property> @@ -30,74 +87,86 @@ <property name="vscrollbar_policy">automatic</property> <property name="shadow_type">in</property> <child> - <widget class="GtkTreeView" id="treeview"> + <object class="GtkTreeView" id="treeview"> <property name="width_request">100</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="headers_visible">False</property> - <signal name="cursor_changed" handler="on_treeview_cursor_changed"/> - </widget> + <signal name="cursor-changed" handler="on_treeview_cursor_changed" swapped="no"/> + </object> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">5</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkNotebook" id="notebook"> + <object class="GtkNotebook" id="notebook"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="show_border">False</property> <child> - <widget class="GtkVBox" id="folder_tab"> + <object class="GtkVBox" id="folder_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox6"> + <object class="GtkVBox" id="vbox6"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox4"> + <object class="GtkHBox" id="hbox8"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image2"> + <object class="GtkImage" id="image2"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">gtk-directory</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label2"> + <object class="GtkLabel" id="label4"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Photo Download Folders</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator1"> + <object class="GtkHSeparator" id="hseparator1"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -105,31 +174,36 @@ </packing> </child> <child> - <widget class="GtkHBox" id="hbox9"> + <object class="GtkHBox" id="hbox9"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label16"> + <object class="GtkLabel" id="label16"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="download_folder_table"> + <object class="GtkTable" id="download_folder_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">7</property> <property name="n_columns">3</property> <child> - <widget class="GtkLabel" id="example_photo_download_path_label"> + <object class="GtkLabel" id="example_photo_download_path_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><i>Example: /home/user/Pictures</i></property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -138,12 +212,13 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label8"> + <object class="GtkLabel" id="label8"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Download Subfolders</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">3</property> <property name="top_attach">3</property> @@ -152,11 +227,12 @@ </packing> </child> <child> - <widget class="GtkLabel" id="lblPhotos1"> + <object class="GtkLabel" id="lblPhotos1"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Download folder:</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -167,14 +243,15 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label7"> + <object class="GtkLabel" id="label7"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">12</property> <property name="label" translatable="yes">Choose the download folder. Subfolders for the downloaded photos will be automatically created in this folder using the structure specified below.</property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -184,25 +261,27 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label5"> + <object class="GtkLabel" id="label5"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Download Folder</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">3</property> <property name="y_options"></property> </packing> </child> <child> - <widget class="GtkLabel" id="photo_subfolder_warning_label"> + <object class="GtkLabel" id="photo_subfolder_warning_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="wrap">True</property> <property name="wrap_mode">word-char</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -212,12 +291,13 @@ </packing> </child> <child> - <widget class="GtkVBox" id="subfolder_vbox"> + <object class="GtkVBox" id="subfolder_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> <placeholder/> </child> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -244,88 +324,105 @@ <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label23"> + <object class="GtkLabel" id="label23"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="menu_label">Download Folders</property> </packing> </child> - <child> - <widget class="GtkLabel" id="download_label"> + <child type="tab"> + <object class="GtkLabel" id="download_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Photo Folders</property> - </widget> + </object> <packing> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="rename_tab"> + <object class="GtkVBox" id="rename_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox7"> + <object class="GtkVBox" id="vbox7"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox5"> + <object class="GtkHBox" id="hbox10"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image3"> + <object class="GtkImage" id="image3"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">gtk-convert</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label4"> + <object class="GtkLabel" id="label6"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Photo Rename</span> </property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator2"> + <object class="GtkHSeparator" id="hseparator2"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -333,70 +430,82 @@ </packing> </child> <child> - <widget class="GtkHBox" id="hbox12"> + <object class="GtkHBox" id="hbox12"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label24"> + <object class="GtkLabel" id="label24"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkVBox" id="rename_vbox"> + <object class="GtkVBox" id="rename_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label42"> + <object class="GtkLabel" id="label42"> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Photo Rename</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkScrolledWindow" id="rename_scrolledwindow"> + <object class="GtkScrolledWindow" id="rename_scrolledwindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscrollbar_policy">automatic</property> <property name="vscrollbar_policy">automatic</property> <property name="window_placement_set">True</property> <child> - <widget class="GtkViewport" id="viewport2"> + <object class="GtkViewport" id="viewport2"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="resize_mode">queue</property> <property name="shadow_type">none</property> <child> - <widget class="GtkVBox" id="rename_table_vbox"> + <object class="GtkVBox" id="rename_table_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> <placeholder/> </child> - </widget> + </object> </child> - </widget> + </object> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkTable" id="rename_example_table"> + <object class="GtkTable" id="rename_example_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">3</property> <property name="n_columns">3</property> <child> - <widget class="GtkLabel" id="label17"> + <object class="GtkLabel" id="label17"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"> </property> - </widget> + </object> <packing> <property name="top_attach">2</property> <property name="bottom_attach">3</property> @@ -404,10 +513,11 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label15"> + <object class="GtkLabel" id="label15"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"> </property> - </widget> + </object> <packing> <property name="top_attach">1</property> <property name="bottom_attach">2</property> @@ -416,13 +526,14 @@ </packing> </child> <child> - <widget class="GtkLabel" id="new_name_label"> + <object class="GtkLabel" id="new_name_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="label">translators please ignore this</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -432,11 +543,12 @@ </packing> </child> <child> - <widget class="GtkLabel" id="original_name_label"> + <object class="GtkLabel" id="original_name_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label">translators please ignore this</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -446,13 +558,14 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label21"> + <object class="GtkLabel" id="label21"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="label" translatable="yes"><i>New:</i></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -463,12 +576,13 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label20"> + <object class="GtkLabel" id="label20"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><i>Original:</i></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -479,109 +593,126 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label14"> + <object class="GtkLabel" id="label14"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Example</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">3</property> <property name="y_padding">12</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label25"> + <object class="GtkLabel" id="label25"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="menu_label">Rename</property> <property name="position">1</property> </packing> </child> - <child> - <widget class="GtkLabel" id="rename_label"> + <child type="tab"> + <object class="GtkLabel" id="rename_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Photo Rename</property> - </widget> + </object> <packing> <property name="position">1</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="folder_tab1"> + <object class="GtkVBox" id="folder_tab1"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox12"> + <object class="GtkVBox" id="vbox12"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox8"> + <object class="GtkHBox" id="hbox11"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image5"> + <object class="GtkImage" id="image5"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">gtk-directory</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label46"> + <object class="GtkLabel" id="label46"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Video Download Folders</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator8"> + <object class="GtkHSeparator" id="hseparator8"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -589,21 +720,25 @@ </packing> </child> <child> - <widget class="GtkHBox" id="folder_videos_cannot_be_downloaded_hbox"> + <object class="GtkHBox" id="folder_videos_cannot_be_downloaded_hbox"> + <property name="can_focus">False</property> <child> - <widget class="GtkLabel" id="folder_videos_cannot_be_downloaded_label"> + <object class="GtkLabel" id="folder_videos_cannot_be_downloaded_label"> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="ypad">10</property> <property name="label" translatable="yes">Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python.</property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -611,29 +746,34 @@ </packing> </child> <child> - <widget class="GtkHBox" id="video_folders_hbox"> + <object class="GtkHBox" id="video_folders_hbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label57"> + <object class="GtkLabel" id="label57"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="video_download_folder_table"> + <object class="GtkTable" id="video_download_folder_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">7</property> <property name="n_columns">3</property> <child> - <widget class="GtkLabel" id="lblPhotos2"> + <object class="GtkLabel" id="lblPhotos2"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Download folder:</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -644,14 +784,15 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label59"> + <object class="GtkLabel" id="label59"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">12</property> <property name="label" translatable="yes">Choose the download folder. Subfolders for the downloaded videos will be automatically created in this folder using the structure specified below.</property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -661,24 +802,26 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label61"> + <object class="GtkLabel" id="label61"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Download Folder</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">3</property> <property name="y_options"></property> </packing> </child> <child> - <widget class="GtkLabel" id="label62"> + <object class="GtkLabel" id="label62"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Download Subfolders</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">3</property> <property name="top_attach">3</property> @@ -687,13 +830,14 @@ </packing> </child> <child> - <widget class="GtkLabel" id="example_video_download_path_label"> + <object class="GtkLabel" id="example_video_download_path_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><i>Example: /home/user/Pictures</i></property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -702,13 +846,14 @@ </packing> </child> <child> - <widget class="GtkLabel" id="video_subfolder_warning_label"> + <object class="GtkLabel" id="video_subfolder_warning_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="wrap">True</property> <property name="wrap_mode">word-char</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -718,12 +863,13 @@ </packing> </child> <child> - <widget class="GtkVBox" id="video_subfolder_vbox"> + <object class="GtkVBox" id="video_subfolder_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> <placeholder/> </child> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -750,89 +896,106 @@ <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label63"> + <object class="GtkLabel" id="label63"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="position">2</property> </packing> </child> - <child> - <widget class="GtkLabel" id="video_download_folder"> + <child type="tab"> + <object class="GtkLabel" id="video_download_folder"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Video Folders</property> - </widget> + </object> <packing> <property name="position">2</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="video_rename_tab"> + <object class="GtkVBox" id="video_rename_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox10"> + <object class="GtkVBox" id="vbox10"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox1"> + <object class="GtkHBox" id="hbox13"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image1"> + <object class="GtkImage" id="image1"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">gtk-convert</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label32"> + <object class="GtkLabel" id="label32"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Video Rename</span> </property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator3"> + <object class="GtkHSeparator" id="hseparator4"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -840,29 +1003,34 @@ </packing> </child> <child> - <widget class="GtkHBox" id="hbox6"> + <object class="GtkHBox" id="hbox14"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label44"> + <object class="GtkLabel" id="label44"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkVBox" id="rename_vbox1"> + <object class="GtkVBox" id="rename_vbox1"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="videos_cannot_be_downloaded_label"> + <object class="GtkLabel" id="videos_cannot_be_downloaded_label"> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python.</property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -870,42 +1038,48 @@ </packing> </child> <child> - <widget class="GtkScrolledWindow" id="video_rename_scrolledwindow"> + <object class="GtkScrolledWindow" id="video_rename_scrolledwindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscrollbar_policy">automatic</property> <property name="vscrollbar_policy">automatic</property> <property name="window_placement_set">True</property> <child> - <widget class="GtkViewport" id="viewport1"> + <object class="GtkViewport" id="viewport1"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="resize_mode">queue</property> <property name="shadow_type">none</property> <child> - <widget class="GtkVBox" id="video_rename_table_vbox"> + <object class="GtkVBox" id="video_rename_table_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> <placeholder/> </child> - </widget> + </object> </child> - </widget> + </object> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkTable" id="video_rename_example_table"> + <object class="GtkTable" id="video_rename_example_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">3</property> <property name="n_columns">3</property> <child> - <widget class="GtkLabel" id="label55"> + <object class="GtkLabel" id="label55"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"> </property> - </widget> + </object> <packing> <property name="top_attach">2</property> <property name="bottom_attach">3</property> @@ -913,10 +1087,11 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label56"> + <object class="GtkLabel" id="label56"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"> </property> - </widget> + </object> <packing> <property name="top_attach">1</property> <property name="bottom_attach">2</property> @@ -925,13 +1100,14 @@ </packing> </child> <child> - <widget class="GtkLabel" id="video_new_name_label"> + <object class="GtkLabel" id="video_new_name_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="label">translators please ignore this</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -941,11 +1117,12 @@ </packing> </child> <child> - <widget class="GtkLabel" id="video_original_name_label"> + <object class="GtkLabel" id="video_original_name_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label">translators please ignore this</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -955,13 +1132,14 @@ </packing> </child> <child> - <widget class="GtkLabel" id="new_video_filename_label"> + <object class="GtkLabel" id="new_video_filename_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="label" translatable="yes"><i>New:</i></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -972,12 +1150,13 @@ </packing> </child> <child> - <widget class="GtkLabel" id="original_video_filename_label"> + <object class="GtkLabel" id="original_video_filename_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><i>Original:</i></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -988,107 +1167,125 @@ </packing> </child> <child> - <widget class="GtkLabel" id="example_video_filename_label"> + <object class="GtkLabel" id="example_video_filename_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Example</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">3</property> <property name="y_padding">12</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label60"> + <object class="GtkLabel" id="label60"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">3</property> </packing> </child> - <child> - <widget class="GtkLabel" id="video_rename_label"> + <child type="tab"> + <object class="GtkLabel" id="video_rename_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Video Rename</property> - </widget> + </object> <packing> <property name="position">3</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="rename_options_tab"> + <object class="GtkVBox" id="rename_options_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox14"> + <object class="GtkVBox" id="vbox14"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox18"> + <object class="GtkHBox" id="hbox18"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image7"> + <object class="GtkImage" id="image7"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="icon_name">input-keyboard</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label10"> + <object class="GtkLabel" id="label10"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Rename Options</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator4"> + <object class="GtkHSeparator" id="hseparator5"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1096,123 +1293,151 @@ </packing> </child> <child> - <widget class="GtkVBox" id="reame_options_vbox"> + <object class="GtkVBox" id="reame_options_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkLabel" id="sequence_number_label"> + <object class="GtkLabel" id="sequence_number_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes"><b>Sequence Numbers</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHBox" id="sequence_number_hbox"> + <object class="GtkHBox" id="sequence_number_hbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="spacer_seq_label"> + <object class="GtkLabel" id="spacer_seq_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xpad">12</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkVBox" id="seq_vbox"> + <object class="GtkVBox" id="seq_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label47"> + <object class="GtkLabel" id="label47"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Specify the time in 24 hour format at which the <i>Downloads today</i> sequence number should be reset.</property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHBox" id="hbox23"> + <object class="GtkHBox" id="hbox23"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkVBox" id="vbox1"> + <object class="GtkVBox" id="vbox3"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkLabel" id="label49"> + <object class="GtkLabel" id="label49"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Day start:</property> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label51"> + <object class="GtkLabel" id="label51"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Downloads today:</property> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label52"> + <object class="GtkLabel" id="label52"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Stored number:</property> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label54"> + <object class="GtkLabel" id="label54"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xpad">6</property> <property name="label" translatable="yes"> </property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkVBox" id="sequence_vbox"> + <object class="GtkVBox" id="sequence_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkHBox" id="hbox22"> + <object class="GtkHBox" id="hbox22"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkSpinButton" id="hour_spinbutton"> + <object class="GtkSpinButton" id="hour_spinbutton"> <property name="visible">True</property> <property name="can_focus">True</property> + <property name="max_length">2</property> + <property name="invisible_char">•</property> <property name="width_chars">2</property> <property name="xalign">1</property> <property name="truncate_multiline">True</property> - <property name="adjustment">0 0 23 1 10 0</property> + <property name="adjustment">hour_adjustment</property> <property name="numeric">True</property> - <signal name="value_changed" handler="on_hour_spinbutton_value_changed"/> - </widget> + <signal name="value-changed" handler="on_hour_spinbutton_value_changed" swapped="no"/> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1220,10 +1445,11 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label50"> + <object class="GtkLabel" id="label50"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">:</property> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1231,16 +1457,18 @@ </packing> </child> <child> - <widget class="GtkSpinButton" id="minute_spinbutton"> + <object class="GtkSpinButton" id="minute_spinbutton"> <property name="visible">True</property> <property name="can_focus">True</property> + <property name="max_length">2</property> + <property name="invisible_char">•</property> <property name="width_chars">2</property> <property name="xalign">1</property> <property name="truncate_multiline">True</property> - <property name="adjustment">0 0 59 1 10 0</property> + <property name="adjustment">minute_adjustment</property> <property name="numeric">True</property> - <signal name="value_changed" handler="on_minute_spinbutton_value_changed"/> - </widget> + <signal name="value-changed" handler="on_minute_spinbutton_value_changed" swapped="no"/> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1248,19 +1476,22 @@ </packing> </child> <child> - <widget class="GtkLabel" id="label53"> + <object class="GtkLabel" id="label53"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"> hh:mm</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">3</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> @@ -1270,200 +1501,237 @@ <child> <placeholder/> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkCheckButton" id="synchronize_raw_jpg_checkbutton"> + <object class="GtkCheckButton" id="synchronize_raw_jpg_checkbutton"> <property name="label" translatable="yes">Synchronize RAW + JPEG sequence numbers</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_synchronize_raw_jpg_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_synchronize_raw_jpg_checkbutton_toggled" swapped="no"/> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label48"> + <object class="GtkLabel" id="label48"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="compatibility_label"> + <object class="GtkLabel" id="compatibility_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes"><b>Compatibility with Other Operating Systems</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">3</property> </packing> </child> <child> - <widget class="GtkHBox" id="compatibility_hbox"> + <object class="GtkHBox" id="compatibility_hbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="compatibility_spacer_label"> + <object class="GtkLabel" id="compatibility_spacer_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xpad">12</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="compatibility_table"> + <object class="GtkTable" id="compatibility_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">2</property> <property name="n_columns">2</property> <child> - <widget class="GtkLabel" id="label9"> + <object class="GtkLabel" id="label9"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Specify whether photo, video and folder names should have any characters removed that are not allowed by other operating systems.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="right_attach">2</property> </packing> </child> <child> - <widget class="GtkCheckButton" id="strip_characters_checkbutton"> + <object class="GtkCheckButton" id="strip_characters_checkbutton"> <property name="label" translatable="yes">Strip incompatible characters</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="use_underline">True</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_strip_characters_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_strip_characters_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">2</property> <property name="top_attach">1</property> <property name="bottom_attach">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label33"> + <object class="GtkLabel" id="label33"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">4</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">4</property> </packing> </child> - <child> - <widget class="GtkLabel" id="rename_options_label"> + <child type="tab"> + <object class="GtkLabel" id="rename_options_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Rename Options</property> - </widget> + </object> <packing> <property name="position">4</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="job_codes_tab"> + <object class="GtkVBox" id="job_codes_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="job_codes_header_vbox"> + <object class="GtkVBox" id="job_codes_header_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox188"> + <object class="GtkHBox" id="hbox188"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image77"> + <object class="GtkImage" id="image77"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">rapid-photo-downloader-jobcode</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label1340"> + <object class="GtkLabel" id="label1340"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Job Codes</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator44"> + <object class="GtkHSeparator" id="hseparator44"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1471,75 +1739,86 @@ </packing> </child> <child> - <widget class="GtkVBox" id="job_codes_vbox"> + <object class="GtkVBox" id="job_codes_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkVBox" id="job_code_vbox"> + <object class="GtkVBox" id="job_code_vbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkLabel" id="job_code_label"> + <object class="GtkLabel" id="job_code_label"> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes"><b>Job Codes</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHBox" id="job_code_hbox"> + <object class="GtkHBox" id="job_code_hbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="job_code_spacer_label"> + <object class="GtkLabel" id="job_code_spacer_label"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkScrolledWindow" id="job_code_scrolledwindow"> + <object class="GtkScrolledWindow" id="job_code_scrolledwindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscrollbar_policy">automatic</property> <property name="vscrollbar_policy">automatic</property> <property name="shadow_type">in</property> <child> - <widget class="GtkTreeView" id="job_code_treeview"> + <object class="GtkTreeView" id="job_code_treeview"> <property name="width_request">250</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="headers_visible">False</property> <property name="rubber_banding">True</property> - </widget> + </object> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkHBox" id="job_code_button_hbox"> + <object class="GtkHBox" id="job_code_button_hbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkVButtonBox" id="job_code_vbuttonbox"> + <object class="GtkVButtonBox" id="job_code_vbuttonbox"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <property name="layout_style">start</property> <child> - <widget class="GtkButton" id="add_job_code_button"> + <object class="GtkButton" id="add_job_code_button"> <property name="label" translatable="yes">_Add...</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> <property name="use_underline">True</property> - <signal name="clicked" handler="on_add_job_code_button_clicked"/> - </widget> + <signal name="clicked" handler="on_add_job_code_button_clicked" swapped="no"/> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1547,14 +1826,15 @@ </packing> </child> <child> - <widget class="GtkButton" id="remove_job_code_button"> + <object class="GtkButton" id="remove_job_code_button"> <property name="label">gtk-remove</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> <property name="use_stock">True</property> - <signal name="clicked" handler="on_remove_job_code_button_clicked"/> - </widget> + <signal name="clicked" handler="on_remove_job_code_button_clicked" swapped="no"/> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1562,112 +1842,133 @@ </packing> </child> <child> - <widget class="GtkButton" id="remove_all_job_code_button"> + <object class="GtkButton" id="remove_all_job_code_button"> <property name="label" translatable="yes" comments="The underscore after the C signifies that the l is the accelerator key. This is the standard 'Clear' button, but I needed to change the accelerator from the standard 'c' to 'l' because the close button also used 'c'">R_emove All</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> <property name="use_underline">True</property> - <signal name="clicked" handler="on_remove_all_job_code_button_clicked"/> - </widget> + <signal name="clicked" handler="on_remove_all_job_code_button_clicked" swapped="no"/> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">5</property> </packing> </child> - <child> - <widget class="GtkLabel" id="job_codes_tab_label"> + <child type="tab"> + <object class="GtkLabel" id="job_codes_tab_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Job Codes</property> - </widget> + </object> <packing> <property name="position">5</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="device_tab"> + <object class="GtkVBox" id="device_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox3"> + <object class="GtkVBox" id="vbox4"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox2"> + <object class="GtkHBox" id="hbox15"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image6"> + <object class="GtkImage" id="image6"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="icon_name">media-flash</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label22"> + <object class="GtkLabel" id="label22"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Devices</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator5"> + <object class="GtkHSeparator" id="hseparator6"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1675,24 +1976,28 @@ </packing> </child> <child> - <widget class="GtkVBox" id="vbox5"> + <object class="GtkVBox" id="vbox5"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label41"> + <object class="GtkLabel" id="label41"> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes"><b>Devices</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label18"> + <object class="GtkLabel" id="label18"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes">Devices are from where to download photos and videos, such as cameras, memory cards or Portable Storage Devices. @@ -1702,39 +2007,46 @@ You can download photos from multiple devices simultaneously, or you can specify <i>If downloading directly from your camera works poorly or not at all, try setting it to PTP mode. If that is not possible, consider using a card reader.</i></property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkHBox" id="hbox14"> + <object class="GtkHBox" id="hbox16"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkLabel" id="label26"> + <object class="GtkLabel" id="label26"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xpad">3</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="devices_table"> + <object class="GtkTable" id="devices_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">3</property> <property name="n_columns">2</property> <property name="row_spacing">3</property> <child> - <widget class="GtkCheckButton" id="autodetect_psd_checkbutton"> + <object class="GtkCheckButton" id="autodetect_psd_checkbutton"> <property name="label" translatable="yes">Automatically detect Portable Storage Devices</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_autodetect_psd_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_autodetect_psd_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -1743,27 +2055,29 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkCheckButton" id="autodetect_device_checkbutton"> + <object class="GtkCheckButton" id="autodetect_device_checkbutton"> <property name="label" translatable="yes">Automatically detect devices</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_autodetect_device_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_autodetect_device_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">2</property> </packing> </child> <child> - <widget class="GtkLabel" id="autodetect_image_devices_label"> + <object class="GtkLabel" id="autodetect_image_devices_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">6</property> <property name="label" translatable="yes">If you enable automatic detection of Portable Storage Devices, the entire device will be scanned for images. On large devices, this could take some time.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -1777,42 +2091,50 @@ You can download photos from multiple devices simultaneously, or you can specify <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label28"> + <object class="GtkLabel" id="label28"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> - <widget class="GtkHBox" id="hbox15"> + <object class="GtkHBox" id="hbox17"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkTable" id="devices2_table"> + <object class="GtkTable" id="devices2_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">2</property> <property name="n_columns">2</property> <child> - <widget class="GtkLabel" id="device_location_label"> + <object class="GtkLabel" id="device_location_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes">Location:</property> - </widget> + </object> <packing> <property name="top_attach">1</property> <property name="bottom_attach">2</property> @@ -1821,15 +2143,16 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="device_location_explanation_label"> + <object class="GtkLabel" id="device_location_explanation_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="ypad">12</property> <property name="label" translatable="yes">If you disable automatic detection, choose the exact location of the images and videos.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="right_attach">2</property> </packing> @@ -1837,8 +2160,10 @@ You can download photos from multiple devices simultaneously, or you can specify <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> @@ -1846,89 +2171,104 @@ You can download photos from multiple devices simultaneously, or you can specify <placeholder/> </child> <child> - <widget class="GtkLabel" id="label30"> + <object class="GtkLabel" id="label30"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">3</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">6</property> </packing> </child> - <child> - <widget class="GtkLabel" id="device_label"> + <child type="tab"> + <object class="GtkLabel" id="device_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Devices</property> - </widget> + </object> <packing> <property name="position">6</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="backup_tab"> + <object class="GtkVBox" id="backup_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkVBox" id="vbox2"> + <object class="GtkVBox" id="vbox8"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox7"> + <object class="GtkHBox" id="hbox19"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image8"> + <object class="GtkImage" id="image8"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="icon_name">drive-removable-media</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label27"> + <object class="GtkLabel" id="label27"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Backup</span> </property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator6"> + <object class="GtkHSeparator" id="hseparator7"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -1936,47 +2276,55 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkVBox" id="vbox9"> + <object class="GtkVBox" id="vbox9"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkLabel" id="label43"> + <object class="GtkLabel" id="label43"> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes"><b>Backup</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHBox" id="hbox20"> + <object class="GtkHBox" id="hbox20"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label38"> + <object class="GtkLabel" id="label38"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="backup_table"> + <object class="GtkTable" id="backup_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">9</property> <property name="n_columns">4</property> <child> - <widget class="GtkLabel" id="backup_location_explanation_label"> + <object class="GtkLabel" id="backup_location_explanation_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="xalign">0</property> <property name="ypad">12</property> <property name="label" translatable="yes">If you disable automatic detection, choose the exact backup location.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">4</property> @@ -1985,15 +2333,16 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkCheckButton" id="auto_detect_backup_checkbutton"> + <object class="GtkCheckButton" id="auto_detect_backup_checkbutton"> <property name="label" translatable="yes">Automatically detect backup devices</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_auto_detect_backup_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_auto_detect_backup_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">4</property> @@ -2003,28 +2352,30 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="label11"> + <object class="GtkLabel" id="label11"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">12</property> <property name="label" translatable="yes">You can have your photos and videos backed up to multiple locations as they are downloaded, e.g. external hard drives.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="right_attach">4</property> <property name="y_options"></property> </packing> </child> <child> - <widget class="GtkCheckButton" id="backup_checkbutton"> + <object class="GtkCheckButton" id="backup_checkbutton"> <property name="label" translatable="yes">Backup photos and videos when downloading</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="use_underline">True</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_backup_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_backup_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">4</property> <property name="top_attach">1</property> @@ -2033,8 +2384,9 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="backup_identifier_explanation_label"> + <object class="GtkLabel" id="backup_identifier_explanation_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">6</property> <property name="label" translatable="yes">Specify the folder in which backups are stored on the device. @@ -2042,7 +2394,7 @@ You can download photos from multiple devices simultaneously, or you can specify <i>Note: this will also be used to determine whether or not the device is used for backups. For each device you wish to use for backing up to, create a folder in it with one of these names.</i></property> <property name="use_markup">True</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">4</property> @@ -2051,11 +2403,12 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="backup_location_label"> + <object class="GtkLabel" id="backup_location_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Backup location:</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">3</property> @@ -2066,11 +2419,12 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="backup_identifier_label"> + <object class="GtkLabel" id="backup_identifier_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Photo backup folder name:</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -2082,14 +2436,15 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="backup_example_label"> + <object class="GtkLabel" id="backup_example_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="ypad">6</property> <property name="label" translatable="yes"><i>Example:</i></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -2099,14 +2454,15 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="example_backup_path_label"> + <object class="GtkLabel" id="example_backup_path_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="yalign">0</property> <property name="ypad">6</property> <property name="label" translatable="yes"><i>/media/externaldrive/Photos</i></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="left_attach">3</property> <property name="right_attach">4</property> @@ -2115,12 +2471,13 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkEntry" id="backup_identifier_entry"> + <object class="GtkEntry" id="backup_identifier_entry"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <signal name="changed" handler="on_backup_identifier_entry_changed"/> - </widget> + <property name="invisible_char">•</property> + <signal name="changed" handler="on_backup_identifier_entry_changed" swapped="no"/> + </object> <packing> <property name="left_attach">3</property> <property name="right_attach">4</property> @@ -2131,11 +2488,12 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="video_backup_identifier_label"> + <object class="GtkLabel" id="video_backup_identifier_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes">Video backup folder name:</property> - </widget> + </object> <packing> <property name="left_attach">2</property> <property name="right_attach">3</property> @@ -2147,13 +2505,13 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkEntry" id="video_backup_identifier_entry"> + <object class="GtkEntry" id="video_backup_identifier_entry"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="invisible_char">●</property> - <signal name="changed" handler="on_video_backup_identifier_entry_changed"/> - </widget> + <property name="invisible_char">•</property> + <signal name="changed" handler="on_video_backup_identifier_entry_changed" swapped="no"/> + </object> <packing> <property name="left_attach">3</property> <property name="right_attach">4</property> @@ -2199,94 +2557,112 @@ You can download photos from multiple devices simultaneously, or you can specify <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label39"> + <object class="GtkLabel" id="label39"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">7</property> </packing> </child> - <child> - <widget class="GtkLabel" id="backup_label"> + <child type="tab"> + <object class="GtkLabel" id="backup_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes">Backup</property> - </widget> + </object> <packing> <property name="position">7</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="automation_tab"> + <object class="GtkVBox" id="automation_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkVBox" id="vbox4"> + <object class="GtkVBox" id="vbox11"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox10"> + <object class="GtkHBox" id="hbox21"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image4"> + <object class="GtkImage" id="image4"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">gtk-execute</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label31"> + <object class="GtkLabel" id="label31"> <property name="visible">True</property> - <property name="label" translatable="yes"><span weight="bold" size="x-large">Automation</span></property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Miscellaneous</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator7"> + <object class="GtkHSeparator" id="hseparator9"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -2294,46 +2670,56 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkVBox" id="vbox11"> + <object class="GtkVBox" id="vbox13"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkLabel" id="label45"> + <object class="GtkLabel" id="label45"> + <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> <property name="label" translatable="yes"><b>Program Automation</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHBox" id="hbox17"> + <object class="GtkHBox" id="hbox24"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkLabel" id="label34"/> + <object class="GtkLabel" id="label34"> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="automation_table"> + <object class="GtkTable" id="automation_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">7</property> <property name="n_columns">3</property> <child> - <widget class="GtkCheckButton" id="auto_unmount_checkbutton"> + <object class="GtkCheckButton" id="auto_unmount_checkbutton"> <property name="label" translatable="yes">Unmount ("eject") device upon download completion</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_auto_unmount_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_auto_unmount_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">3</property> <property name="top_attach">2</property> @@ -2341,29 +2727,31 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkCheckButton" id="auto_startup_checkbutton"> + <object class="GtkCheckButton" id="auto_startup_checkbutton"> <property name="label" translatable="yes">Start downloading at program startup</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_auto_startup_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_auto_startup_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">3</property> </packing> </child> <child> - <widget class="GtkCheckButton" id="auto_insertion_checkbutton"> + <object class="GtkCheckButton" id="auto_insertion_checkbutton"> <property name="label" translatable="yes">Start downloading upon device insertion</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_auto_insertion_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_auto_insertion_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">3</property> <property name="top_attach">1</property> @@ -2371,14 +2759,15 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkCheckButton" id="auto_exit_checkbutton"> - <property name="label" translatable="yes">Exit program if download completes without any warnings or errors</property> + <object class="GtkCheckButton" id="auto_exit_checkbutton"> + <property name="label" translatable="yes">Exit program when download completes</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_auto_exit_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_auto_exit_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">3</property> <property name="top_attach">3</property> @@ -2386,16 +2775,34 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkCheckButton" id="auto_delete_checkbutton"> + <object class="GtkCheckButton" id="auto_delete_checkbutton"> <property name="label" translatable="yes">Delete photos and videos from device upon download completion</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_auto_delete_checkbutton_toggled"/> - </widget> + <signal name="toggled" handler="on_auto_delete_checkbutton_toggled" swapped="no"/> + </object> <packing> <property name="right_attach">3</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="auto_exit_force_checkbutton"> + <property name="label" translatable="yes">Exit program even if download had warnings or errors</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_auto_exit_force_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> <property name="top_attach">4</property> <property name="bottom_attach">5</property> </packing> @@ -2415,101 +2822,175 @@ You can download photos from multiple devices simultaneously, or you can specify <child> <placeholder/> </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">24</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label35"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <object class="GtkHBox" id="hbox25"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label19"> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_columns">3</property> <child> <placeholder/> </child> - </widget> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">24</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label35"> + <object class="GtkLabel" id="label29"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="padding">12</property> - <property name="position">1</property> + <property name="position">3</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">8</property> </packing> </child> - <child> - <widget class="GtkLabel" id="automation_label"> + <child type="tab"> + <object class="GtkLabel" id="miscillaneous_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="label" translatable="yes">Automation</property> - </widget> + <property name="label" translatable="yes">Miscillaneous</property> + </object> <packing> <property name="position">8</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> <child> - <widget class="GtkVBox" id="error_tab"> + <object class="GtkVBox" id="error_tab"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkVBox" id="vbox8"> + <object class="GtkVBox" id="vbox16"> <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <widget class="GtkHBox" id="hbox13"> + <object class="GtkHBox" id="hbox26"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">6</property> <child> - <widget class="GtkImage" id="image9"> + <object class="GtkImage" id="image9"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="stock">gtk-dialog-error</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkLabel" id="label40"> + <object class="GtkLabel" id="label40"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="label" translatable="yes"><span weight="bold" size="x-large">Error Handling</span></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkHSeparator" id="hseparator9"> + <object class="GtkHSeparator" id="hseparator10"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> <property name="fill">False</property> @@ -2517,29 +2998,34 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkHBox" id="hbox19"> + <object class="GtkHBox" id="hbox27"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="spacing">12</property> <child> - <widget class="GtkLabel" id="label36"> + <object class="GtkLabel" id="label36"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> - <widget class="GtkTable" id="error_table"> + <object class="GtkTable" id="error_table"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="n_rows">14</property> <property name="n_columns">2</property> <child> - <widget class="GtkLabel" id="label1"> + <object class="GtkLabel" id="label13"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="label" translatable="yes"> </property> - </widget> + </object> <packing> <property name="top_attach">2</property> <property name="bottom_attach">3</property> @@ -2548,29 +3034,30 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="label12"> + <object class="GtkLabel" id="label37"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="label" translatable="yes"><b>Photo and Video Name Conflicts</b></property> <property name="use_markup">True</property> - </widget> + </object> <packing> <property name="right_attach">2</property> <property name="y_options">GTK_FILL</property> </packing> </child> <child> - <widget class="GtkRadioButton" id="add_identifier_radiobutton"> + <object class="GtkRadioButton" id="add_identifier_radiobutton"> <property name="label" translatable="yes">Add unique identifier</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="use_underline">True</property> <property name="active">True</property> <property name="draw_indicator">True</property> - <property name="group">skip_download_radiobutton</property> - <signal name="toggled" handler="on_add_identifier_radiobutton_toggled"/> - </widget> + <signal name="toggled" handler="on_add_identifier_radiobutton_toggled" swapped="no"/> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -2580,15 +3067,16 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkRadioButton" id="skip_download_radiobutton"> + <object class="GtkRadioButton" id="skip_download_radiobutton"> <property name="label" translatable="yes">Skip download</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="use_underline">True</property> - <property name="active">True</property> <property name="draw_indicator">True</property> - </widget> + <property name="group">add_identifier_radiobutton</property> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -2598,13 +3086,14 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="label13"> + <object class="GtkLabel" id="label58"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">12</property> <property name="label" translatable="yes">When a photo or video of the same name has already been downloaded, choose whether to skip downloading the file, or to add a unique indentifier.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -2614,13 +3103,14 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkLabel" id="label6"> + <object class="GtkLabel" id="label64"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">12</property> <property name="label" translatable="yes">When backing up, choose whether to overwrite a file on the backup device that has the same name, or skip backing it up.</property> <property name="wrap">True</property> - </widget> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -2630,16 +3120,16 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkRadioButton" id="backup_duplicate_overwrite_radiobutton"> + <object class="GtkRadioButton" id="backup_duplicate_overwrite_radiobutton"> <property name="label" translatable="yes">Overwrite</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> - <property name="active">True</property> + <property name="use_action_appearance">False</property> <property name="draw_indicator">True</property> <property name="group">backup_duplicate_skip_radiobutton</property> - <signal name="toggled" handler="on_backup_duplicate_overwrite_radiobutton_toggled"/> - </widget> + <signal name="toggled" handler="on_backup_duplicate_overwrite_radiobutton_toggled" swapped="no"/> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -2648,15 +3138,16 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <widget class="GtkRadioButton" id="backup_duplicate_skip_radiobutton"> + <object class="GtkRadioButton" id="backup_duplicate_skip_radiobutton"> <property name="label" translatable="yes">Skip</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> <property name="active">True</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_backup_duplicate_skip_radiobutton_toggled"/> - </widget> + <signal name="toggled" handler="on_backup_duplicate_skip_radiobutton_toggled" swapped="no"/> + </object> <packing> <property name="left_attach">1</property> <property name="right_attach">2</property> @@ -2721,52 +3212,61 @@ You can download photos from multiple devices simultaneously, or you can specify <child> <placeholder/> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> - <widget class="GtkLabel" id="label37"> + <object class="GtkLabel" id="label65"> <property name="visible">True</property> - </widget> + <property name="can_focus">False</property> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="position">2</property> </packing> </child> - </widget> + </object> <packing> <property name="expand">False</property> + <property name="fill">True</property> <property name="padding">12</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> <property name="position">9</property> </packing> </child> - <child> - <widget class="GtkLabel" id="error_label"> + <child type="tab"> + <object class="GtkLabel" id="error_label"> <property name="visible">True</property> + <property name="can_focus">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="label" translatable="yes">Error Handling</property> - </widget> + </object> <packing> <property name="position">9</property> <property name="tab_fill">False</property> - <property name="type">tab</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">6</property> <property name="position">1</property> </packing> </child> - </widget> + </object> <packing> + <property name="expand">True</property> + <property name="fill">True</property> <property name="padding">5</property> <property name="position">1</property> </packing> @@ -2774,786 +3274,11 @@ You can download photos from multiple devices simultaneously, or you can specify <child> <placeholder/> </child> - <child internal-child="action_area"> - <widget class="GtkHButtonBox" id="dialog-action_area2"> - <property name="visible">True</property> - <property name="layout_style">end</property> - <child> - <widget class="GtkButton" id="help_button"> - <property name="label">gtk-help</property> - <property name="response_id">-11</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_stock">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - <property name="secondary">True</property> - </packing> - </child> - <child> - <widget class="GtkButton" id="close_button"> - <property name="label">gtk-close</property> - <property name="response_id">-7</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="use_stock">True</property> - <signal name="clicked" handler="on_close_button_clicked"/> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - </widget> - </child> - </widget> - <widget class="GtkAboutDialog" id="about"> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="border_width">5</property> - <property name="destroy_with_parent">True</property> - <property name="icon">rapid-photo-downloader.svg</property> - <property name="type_hint">normal</property> - <property name="program_name">Rapid Photo Downloader</property> - <property name="copyright" translatable="yes">Copyright Damon Lynch 2007-11</property> - <property name="comments" translatable="yes">Import your photos and videos efficiently and reliably</property> - <property name="website">http://www.damonlynch.net/rapid</property> - <property name="license" translatable="yes">Rapid Photo Downloader 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. - -Rapid Photo Downloader 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 Rapid Photo Downloader; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</property> - <property name="authors">Damon Lynch <damonlynch@gmail.com></property> - <property name="translator_credits">Anton Alyab'ev <subeditor@dolgopa.org> -Lőrincz András <level.andrasnak@gmail.com> -Michel Ange <michelange@wanadoo.fr> -Alain J. Baudrez <a.baudrez@gmail.com> -Bert <crinbert@yahoo.com> -Martin Dahl Moe -Martin Egger <martin.egger@gmx.net> -Miroslav Matejaš <silverspace@ubuntu-hr.org> -Nicolás M. Zahlut <nzahlut@live.com> -Erik M -Jose Luis Navarro <jlnavarro111@gmail.com> -Tomas Novak <kuvaly@seznam.cz> -Abel O'Rian <abel.orian@gmail.com> -Balazs Oveges <ovegesb@freemail.hu> -Daniel Paessler <daniel@paessler.org> -Miloš Popović <gpopac@gmail.com> -Michal Predotka <mpredotka@googlemail.com> -Ye Qing <allen19920930@gmail.com> -Luca Reverberi <thereve@gmail.com> -Mikko Ruohola <polarfox@polarfox.net> -Sergiy Gavrylov <sergiovana@bigmir.net> -Sergei Sedov <sedov@webmail.perm.ru> -Marco Solari <marcosolari@gmail.com> -Toni Lähdekorpi <toni@lygon.net> -Ulf Urdén <ulf.urden@purplescout.com> -Julien Valroff <julien@kirya.net> -Aron Xu <happyaron.xu@gmail.com> -梁其学 <yalongbay@gmail.com></property> - <property name="logo">rapid-photo-downloader.svg</property> - <property name="wrap_license">True</property> - <child internal-child="vbox"> - <widget class="GtkVBox" id="dialog-vbox1"> - <property name="visible">True</property> - <property name="spacing">2</property> - <child> - <placeholder/> - </child> - <child internal-child="action_area"> - <widget class="GtkHButtonBox" id="dialog-action_area1"> - <property name="visible">True</property> - <property name="layout_style">end</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - </widget> - </child> - </widget> - <widget class="GtkWindow" id="rapidapp"> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="title" translatable="yes">Rapid Photo Downloader</property> - <property name="default_width">650</property> - <property name="icon">rapid-photo-downloader.svg</property> - <signal name="destroy" handler="on_rapidapp_destroy"/> - <signal name="window_state_event" handler="on_rapidapp_window_state_event"/> - <child> - <widget class="GtkVBox" id="vbox10"> - <property name="visible">True</property> - <child> - <widget class="GtkMenuBar" id="menubar3"> - <property name="visible">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <child> - <widget class="GtkMenuItem" id="menuitem7"> - <property name="visible">True</property> - <property name="label" translatable="yes">_File</property> - <property name="use_underline">True</property> - <child> - <widget class="GtkMenu" id="menuitem7_menu"> - <child> - <widget class="GtkImageMenuItem" id="menu_download_pause"> - <property name="label" translatable="yes">Download / Pause</property> - <property name="visible">True</property> - <property name="use_stock">False</property> - <signal name="activate" handler="on_download_button_clicked"/> - <accelerator key="Return" signal="activate" modifiers="GDK_CONTROL_MASK"/> - <child internal-child="image"> - <widget class="GtkImage" id="image1"> - <property name="visible">True</property> - <property name="stock">gtk-convert</property> - </widget> - </child> - </widget> - </child> - <child> - <widget class="GtkImageMenuItem" id="menu_refresh"> - <property name="label">gtk-refresh</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <signal name="activate" handler="on_menu_refresh_activate"/> - <accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkImageMenuItem" id="menu_preferences"> - <property name="label">gtk-preferences</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <signal name="activate" handler="on_menu_preferences_activate"/> - <accelerator key="p" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkImageMenuItem" id="menu_quit"> - <property name="label">gtk-quit</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <signal name="activate" handler="on_rapidapp_destroy"/> - </widget> - </child> - </widget> - </child> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="select_menuitem"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Select</property> - <property name="use_underline">True</property> - <child> - <widget class="GtkMenu" id="select_menu"> - <child> - <widget class="GtkMenuItem" id="menu_select_all"> - <property name="visible">True</property> - <property name="label" translatable="yes">Select _All</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_select_all_activate"/> - <accelerator key="a" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_select_all_photos"> - <property name="visible">True</property> - <property name="label" translatable="yes">Select All Pho_tos</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_select_all_photos_activate"/> - <accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_select_all_videos"> - <property name="visible">True</property> - <property name="label" translatable="yes">Select All Vi_deos</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_select_all_videos_activate"/> - <accelerator key="d" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_select_none"> - <property name="visible">True</property> - <property name="label" translatable="yes">Se_lect None</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_select_none_activate"/> - <accelerator key="l" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkSeparatorMenuItem" id="menu_select_sep"> - <property name="visible">True</property> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_select_all_without_job_code"> - <property name="visible">True</property> - <property name="label" translatable="yes">Select All Without _Job Code</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_select_all_without_job_code_activate"/> - <accelerator key="j" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_select_all_with_job_code"> - <property name="visible">True</property> - <property name="label" translatable="yes">Select All Wit_h Job Code</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_select_all_with_job_code_activate"/> - <accelerator key="h" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - </widget> - </child> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menuitem10"> - <property name="visible">True</property> - <property name="label" translatable="yes">_View</property> - <property name="use_underline">True</property> - <child> - <widget class="GtkMenu" id="menuitem10_menu"> - <child> - <widget class="GtkCheckMenuItem" id="menu_display_selection"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Preview</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_display_selection_toggled"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_preview_columns"> - <property name="visible">True</property> - <property name="label" translatable="yes">P_review Columns</property> - <property name="use_underline">True</property> - <child> - <widget class="GtkMenu" id="menu1"> - <property name="visible">True</property> - <child> - <widget class="GtkCheckMenuItem" id="menu_type_column"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Type</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_type_column_toggled"/> - </widget> - </child> - <child> - <widget class="GtkCheckMenuItem" id="menu_size_column"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Size</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_size_column_toggled"/> - </widget> - </child> - <child> - <widget class="GtkCheckMenuItem" id="menu_device_column"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Device</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_device_column_toggled"/> - </widget> - </child> - <child> - <widget class="GtkCheckMenuItem" id="menu_filename_column"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Filename</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_filename_column_toggled"/> - </widget> - </child> - <child> - <widget class="GtkCheckMenuItem" id="menu_path_column"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Path</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_path_column_toggled"/> - </widget> - </child> - </widget> - </child> - </widget> - </child> - <child> - <widget class="GtkCheckMenuItem" id="menu_preview_folders"> - <property name="visible">True</property> - <property name="label" translatable="yes">Preview _Folders</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_preview_folders_toggled"/> - </widget> - </child> - <child> - <widget class="GtkSeparatorMenuItem" id="seperator40"> - <property name="visible">True</property> - </widget> - </child> - <child> - <widget class="GtkImageMenuItem" id="menu_zoom_in"> - <property name="label">gtk-zoom-in</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <signal name="activate" handler="on_menu_zoom_in_activate"/> - <accelerator key="plus" signal="activate" modifiers="GDK_CONTROL_MASK"/> - <accelerator key="equal" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkImageMenuItem" id="menu_zoom_out"> - <property name="label">gtk-zoom-out</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <signal name="activate" handler="on_menu_zoom_out_activate"/> - <accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </widget> - </child> - <child> - <widget class="GtkSeparatorMenuItem" id="seperator20"> - <property name="visible">True</property> - </widget> - </child> - <child> - <widget class="GtkCheckMenuItem" id="menu_log_window"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Error Log</property> - <property name="use_underline">True</property> - <signal name="toggled" handler="on_menu_log_window_toggled"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_clear"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Clear Completed Downloads</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_clear_activate"/> - </widget> - </child> - <child> - <widget class="GtkSeparatorMenuItem" id="separator5"> - <property name="visible">True</property> - </widget> - </child> - </widget> - </child> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="help_menuitem"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Help</property> - <property name="use_underline">True</property> - <child> - <widget class="GtkMenu" id="help_menu"> - <child> - <widget class="GtkImageMenuItem" id="menu_get_help_online"> - <property name="label" translatable="yes">_Get Help Online...</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">False</property> - <signal name="activate" handler="on_menu_get_help_online_activate"/> - <accelerator key="F1" signal="activate"/> - <child internal-child="image"> - <widget class="GtkImage" id="image2"> - <property name="visible">True</property> - <property name="icon_name">help</property> - </widget> - </child> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_report_problem"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Report a Problem...</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_report_problem_activate"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_donate"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Make a Donation...</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_donate_activate"/> - </widget> - </child> - <child> - <widget class="GtkMenuItem" id="menu_translate"> - <property name="visible">True</property> - <property name="label" translatable="yes">_Translate this Application...</property> - <property name="use_underline">True</property> - <signal name="activate" handler="on_menu_translate_activate"/> - </widget> - </child> - <child> - <widget class="GtkSeparatorMenuItem" id="separator1"> - <property name="visible">True</property> - </widget> - </child> - <child> - <widget class="GtkImageMenuItem" id="menu_about"> - <property name="label">gtk-about</property> - <property name="visible">True</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <signal name="activate" handler="on_menu_about_activate"/> - </widget> - </child> - </widget> - </child> - </widget> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <widget class="GtkVBox" id="main_vbox"> - <property name="visible">True</property> - <property name="spacing">12</property> - <child> - <widget class="GtkVPaned" id="main_vpaned"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <child> - <widget class="GtkVBox" id="device_vbox"> - <property name="visible">True</property> - <child> - <widget class="GtkHBox" id="media_collection_area_hbox"> - <property name="visible">True</property> - <property name="spacing">6</property> - <child> - <widget class="GtkScrolledWindow" id="media_collection_scrolledwindow"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hscrollbar_policy">automatic</property> - <property name="vscrollbar_policy">automatic</property> - <child> - <widget class="GtkViewport" id="media_collection_viewport"> - <property name="visible">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="resize_mode">queue</property> - <child> - <widget class="GtkVBox" id="media_collection_vbox"> - <property name="visible">True</property> - <child> - <placeholder/> - </child> - </widget> - </child> - </widget> - </child> - </widget> - <packing> - <property name="padding">12</property> - <property name="position">0</property> - </packing> - </child> - </widget> - <packing> - <property name="position">0</property> - </packing> - </child> - </widget> - <packing> - <property name="resize">False</property> - <property name="shrink">False</property> - </packing> - </child> - <child> - <widget class="GtkTable" id="selection_table"> - <property name="visible">True</property> - <property name="n_rows">2</property> - <property name="row_spacing">3</property> - <child> - <widget class="GtkHBox" id="selection_hbox"> - <property name="visible">True</property> - <child> - <placeholder/> - </child> - </widget> - <packing> - <property name="top_attach">1</property> - <property name="bottom_attach">2</property> - </packing> - </child> - <child> - <placeholder/> - </child> - </widget> - <packing> - <property name="resize">True</property> - <property name="shrink">False</property> - </packing> - </child> - </widget> - <packing> - <property name="position">0</property> - </packing> - </child> - <child> - <widget class="GtkHBox" id="buttons_hbox"> - <property name="visible">True</property> - <child> - <placeholder/> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - </widget> - <packing> - <property name="padding">12</property> - <property name="position">1</property> - </packing> - </child> - <child> - <widget class="GtkHBox" id="statusbar_hbox"> - <property name="visible">True</property> - <child> - <widget class="GtkHBox" id="download_progressbar_hbox"> - <property name="visible">True</property> - <child> - <placeholder/> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <widget class="GtkStatusbar" id="rapid_statusbar"> - <property name="visible">True</property> - <property name="has_resize_grip">False</property> - </widget> - <packing> - <property name="position">1</property> - </packing> - </child> - <child> - <widget class="GtkVSeparator" id="vseparator1"> - <property name="visible">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">2</property> - </packing> - </child> - <child> - <widget class="GtkVBox" id="vbox15"> - <property name="visible">True</property> - <child> - <widget class="GtkHSeparator" id="hseparator3"> - <property name="visible">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <widget class="GtkHBox" id="hbox2"> - <property name="visible">True</property> - <child> - <widget class="GtkLabel" id="speed_label"> - <property name="visible">True</property> - <property name="label" translatable="yes"> </property> - <property name="width_chars">9</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <widget class="GtkEventBox" id="error_eventbox"> - <property name="visible">True</property> - <signal name="button_press_event" handler="on_error_eventbox_button_press_event"/> - <child> - <widget class="GtkHBox" id="hbox1"> - <property name="visible">True</property> - <child> - <widget class="GtkVSeparator" id="warning_vseparator"> - <property name="visible">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <widget class="GtkImage" id="error_image"> - <property name="visible">True</property> - <property name="xpad">3</property> - <property name="stock">gtk-dialog-error</property> - <property name="icon-size">1</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - <child> - <widget class="GtkImage" id="warning_image"> - <property name="visible">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="xpad">3</property> - <property name="stock">gtk-dialog-warning</property> - <property name="icon-size">1</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">2</property> - </packing> - </child> - </widget> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - </widget> - <packing> - <property name="position">1</property> - </packing> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">3</property> - </packing> - </child> - <child> - <widget class="GtkVSeparator" id="vseparator2"> - <property name="visible">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="position">4</property> - </packing> - </child> - <child> - <widget class="GtkStatusbar" id="statusbar1"> - <property name="width_request">15</property> - <property name="visible">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">5</property> - </packing> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="pack_type">end</property> - <property name="position">2</property> - </packing> - </child> - </widget> - </child> - </widget> - <widget class="GtkDialog" id="logdialog"> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="border_width">5</property> - <property name="title" translatable="yes">Error Log</property> - <property name="default_width">600</property> - <property name="default_height">400</property> - <property name="destroy_with_parent">True</property> - <property name="icon">rapid-photo-downloader.svg</property> - <property name="type_hint">dialog</property> - <signal name="close" handler="on_logdialog_close"/> - <signal name="response" handler="on_logdialog_response"/> - <child internal-child="vbox"> - <widget class="GtkVBox" id="dialog-vbox4"> - <property name="visible">True</property> - <property name="spacing">2</property> - <child> - <widget class="GtkScrolledWindow" id="log_scrolledwindow"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hscrollbar_policy">automatic</property> - <property name="vscrollbar_policy">automatic</property> - <child> - <widget class="GtkViewport" id="viewport1"> - <property name="visible">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <child> - <widget class="GtkTextView" id="log_textview"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="editable">False</property> - <property name="cursor_visible">False</property> - </widget> - </child> - </widget> - </child> - </widget> - <packing> - <property name="position">1</property> - </packing> - </child> - <child internal-child="action_area"> - <widget class="GtkHButtonBox" id="dialog-action_area4"> - <property name="visible">True</property> - <property name="layout_style">end</property> - <child> - <placeholder/> - </child> - <child> - <widget class="GtkButton" id="button3"> - <property name="label">gtk-close</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="use_stock">True</property> - </widget> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - </widget> - <packing> - <property name="expand">False</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - </widget> + </object> </child> - </widget> -</glade-interface> + <action-widgets> + <action-widget response="-11">help_button1</action-widget> + <action-widget response="-7">close_button</action-widget> + </action-widgets> + </object> +</interface> diff --git a/rapid/glade3/rapid-photo-downloader-download-pending.png b/rapid/glade3/rapid-photo-downloader-download-pending.png Binary files differnew file mode 100644 index 0000000..e12cc69 --- /dev/null +++ b/rapid/glade3/rapid-photo-downloader-download-pending.png diff --git a/rapid/glade3/rapid-photo-downloader-download-pending.svg b/rapid/glade3/rapid-photo-downloader-download-pending.svg deleted file mode 100644 index d6127b7..0000000 --- a/rapid/glade3/rapid-photo-downloader-download-pending.svg +++ /dev/null @@ -1,187 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - version="1.0" - width="16" - height="16" - id="svg4136" - inkscape:version="0.47 r22583" - sodipodi:docname="rapid-photo-downloader-image-loading.svg"> - <metadata - id="metadata33"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1089" - id="namedview31" - showgrid="false" - inkscape:zoom="9.8333333" - inkscape:cx="12" - inkscape:cy="12" - inkscape:window-x="0" - inkscape:window-y="24" - inkscape:window-maximized="1" - inkscape:current-layer="svg4136" /> - <defs - id="defs4138"> - <inkscape:perspective - sodipodi:type="inkscape:persp3d" - inkscape:vp_x="0 : 12 : 1" - inkscape:vp_y="0 : 1000 : 0" - inkscape:vp_z="24 : 12 : 1" - inkscape:persp3d-origin="12 : 8 : 1" - id="perspective35" /> - <linearGradient - id="linearGradient8838"> - <stop - id="stop8840" - style="stop-color:black;stop-opacity:1" - offset="0" /> - <stop - id="stop8842" - style="stop-color:black;stop-opacity:0" - offset="1" /> - </linearGradient> - <radialGradient - cx="62.625" - cy="4.625" - r="10.625" - fx="62.625" - fy="4.625" - id="radialGradient5323" - xlink:href="#linearGradient8838" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(1,0,0,0.341176,0,3.047059)" /> - <linearGradient - id="linearGradient5354"> - <stop - id="stop5356" - style="stop-color:#3f3f3f;stop-opacity:1" - offset="0" /> - <stop - id="stop5358" - style="stop-color:black;stop-opacity:1" - offset="1" /> - </linearGradient> - <linearGradient - x1="19.176617" - y1="13.479795" - x2="19.176617" - y2="45.358662" - id="linearGradient5130" - xlink:href="#linearGradient5354" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(0.4916775,0,0,0.4916774,0.6986745,-0.3018277)" /> - <linearGradient - id="linearGradient37935"> - <stop - id="stop37937" - style="stop-color:#929292;stop-opacity:1" - offset="0" /> - <stop - id="stop37939" - style="stop-color:#4a4a4a;stop-opacity:1" - offset="1" /> - </linearGradient> - <linearGradient - x1="28.771276" - y1="12.91806" - x2="28.771276" - y2="45.347591" - id="linearGradient5128" - xlink:href="#linearGradient37935" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(0.4916775,0,0,0.4916774,0.6986745,-0.3018277)" /> - <linearGradient - id="linearGradient2145"> - <stop - id="stop2147" - style="stop-color:#fffffd;stop-opacity:1" - offset="0" /> - <stop - id="stop2149" - style="stop-color:#cbcbc9;stop-opacity:1" - offset="1" /> - </linearGradient> - <radialGradient - cx="11.901996" - cy="10.045444" - r="29.292715" - fx="11.901996" - fy="10.045444" - id="radialGradient5350" - xlink:href="#linearGradient2145" - gradientUnits="userSpaceOnUse" /> - </defs> - <g - id="layer1" - transform="matrix(0.652174,0,0,0.65491337,-0.15217376,0.2820791)"> - <path - d="m 73.25,4.625 a 10.625,3.625 0 1 1 -21.25,0 10.625,3.625 0 1 1 21.25,0 z" - transform="matrix(1.0823528,0,0,1.2906765,-55.282346,13.351919)" - id="path2774" - style="opacity:0.56043958;fill:url(#radialGradient5323);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999988;marker:none;visibility:visible;display:inline;overflow:visible" /> - <path - d="m 12.492145,1.4999735 c -5.5190685,0 -9.9921705,4.473101 -9.9921705,9.9921715 0,5.519069 4.473102,10.007882 9.9921705,10.007881 5.519068,0 10.007887,-4.488812 10.007882,-10.007881 0,-5.5190705 -4.488814,-9.9921715 -10.007882,-9.9921715 z" - id="path2555" - style="fill:url(#linearGradient5128);fill-opacity:1;stroke:url(#linearGradient5130);stroke-width:0.99994898;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /> - <path - d="m 31.160714,16.910715 a 14.910714,14.910714 0 1 1 -29.8214281,0 14.910714,14.910714 0 1 1 29.8214281,0 z" - transform="matrix(0.5700599,0,0,0.5700599,3.2365269,1.8598792)" - id="path35549" - style="fill:url(#radialGradient5350);fill-opacity:1;fill-rule:evenodd;stroke:none" /> - <path - d="m 12.5,6.4999999 c 0,-1.3926725 0,-1.5690116 0,-1.5690116" - id="path2308" - style="fill:#1f1f1f;fill-opacity:1;fill-rule:evenodd;stroke:#727272;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - <path - d="M 12.194529,11.713855 17.596253,6.3122524" - id="path2312" - style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - <path - d="M 13.071561,11.753718 9.4637681,8.1460064" - id="path2314" - style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - <path - d="M 12.314125,11.434805 18.465311,9.7866365" - id="path2316" - style="fill:#ff0000;fill-rule:evenodd;stroke:#ff0000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - <path - d="m 12.5,18.284507 c 0,-1.392673 0,-1.569012 0,-1.569012" - id="path5368" - style="fill:#121212;fill-opacity:1;fill-rule:evenodd;stroke:#121212;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - <path - d="m 17.5,11.5 c 1.392673,0 1.569013,0 1.569013,0" - id="path5370" - style="fill:#1f1f1f;fill-opacity:1;fill-rule:evenodd;stroke:#1f1f1f;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - <path - d="m 5.7331351,11.5 c 1.3613563,0 1.5337301,0 1.5337301,0" - id="path5372" - style="fill:#1f1f1f;fill-opacity:1;fill-rule:evenodd;stroke:#5f5f5f;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - </g> -</svg> diff --git a/rapid/glade3/rapid.ui b/rapid/glade3/rapid.ui new file mode 100644 index 0000000..bd9638e --- /dev/null +++ b/rapid/glade3/rapid.ui @@ -0,0 +1,993 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.20"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkAction" id="about_action"> + <property name="label" translatable="yes">About...</property> + <property name="stock_id">gtk-about</property> + <signal name="activate" handler="on_about_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="check_all_action"> + <property name="label" translatable="yes">Check All</property> + <signal name="activate" handler="on_check_all_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="check_all_photos_action"> + <property name="label" translatable="yes">Check All Photos</property> + <signal name="activate" handler="on_check_all_photos_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="check_all_videos_action"> + <property name="label" translatable="yes">Check All Videos</property> + <signal name="activate" handler="on_check_all_videos_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="donate_action"> + <property name="label" translatable="yes">Make a Donation...</property> + <signal name="activate" handler="on_donate_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="download_action"> + <property name="label">Download</property> + <property name="icon_name">system-run</property> + <property name="sensitive">False</property> + <signal name="activate" handler="on_download_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="get_help_action"> + <property name="label" translatable="yes">Get Help Online...</property> + <property name="stock_id">gtk-help</property> + <signal name="activate" handler="on_get_help_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="help_action"> + <property name="label">Help</property> + <signal name="activate" handler="on_help_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="next_image_action"> + <property name="label" translatable="yes">Next File</property> + <property name="tooltip">Next file</property> + <property name="stock_id">gtk-go-forward</property> + <property name="always_show_image">True</property> + <signal name="activate" handler="on_next_image_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="preferences_action"> + <property name="label">Preferences</property> + <signal name="activate" handler="on_preferences_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="prev_image_action"> + <property name="label" translatable="yes">Previous File</property> + <property name="tooltip">Previous file</property> + <property name="stock_id">gtk-go-back</property> + <property name="always_show_image">True</property> + <signal name="activate" handler="on_prev_image_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="quit_action"> + <property name="label" translatable="yes">Quit</property> + <property name="stock_id">gtk-quit</property> + <signal name="activate" handler="on_quit_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="refresh_action"> + <property name="label" translatable="yes">Refresh</property> + <property name="stock_id">gtk-refresh</property> + <signal name="activate" handler="on_refresh_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="report_problem_action"> + <property name="label" translatable="yes">Report a Problem...</property> + <property name="stock_id">gtk-dialog-warning</property> + <signal name="activate" handler="on_report_problem_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="show_image_action"> + <property name="tooltip">Show image</property> + <signal name="activate" handler="on_show_image_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="show_thumbnails_action"> + <property name="tooltip">Show thumbnails</property> + <signal name="activate" handler="on_show_thumbnails_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="translate_action"> + <property name="label" translatable="yes">Translate this Application...</property> + <signal name="activate" handler="on_translate_action_activate" swapped="no"/> + </object> + <object class="GtkAction" id="uncheck_all_action"> + <property name="label" translatable="yes">Uncheck All</property> + <signal name="activate" handler="on_uncheck_all_action_activate" swapped="no"/> + </object> + <object class="GtkWindow" id="rapidapp"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Rapid Photo Downloader</property> + <property name="icon">rapid-photo-downloader.svg</property> + <signal name="destroy" handler="on_rapidapp_destroy" swapped="no"/> + <signal name="window-state-event" handler="on_rapidapp_window_state_event" swapped="no"/> + <child> + <object class="GtkVBox" id="rapidapp_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuBar" id="menubar3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="ubuntu_local">True</property> + <child> + <object class="GtkMenuItem" id="menuitem7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="menuitem7_menu"> + <property name="can_focus">False</property> + <property name="ubuntu_local">True</property> + <child> + <object class="GtkImageMenuItem" id="menu_download_pause"> + <property name="label" translatable="yes">Download / Pause</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">download_action</property> + <property name="use_stock">False</property> + <accelerator key="Return" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menu_refresh"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">refresh_action</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menu_preferences"> + <property name="label">gtk-preferences</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">preferences_action</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="p" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menu_quit"> + <property name="label">gtk-quit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">quit_action</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="q" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="select_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Select</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="select_menu"> + <property name="can_focus">False</property> + <property name="ubuntu_local">True</property> + <child> + <object class="GtkMenuItem" id="menu_check_all"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">check_all_action</property> + <accelerator key="a" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_check_all_photos"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">check_all_photos_action</property> + <accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_check_all_videos"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">check_all_videos_action</property> + <accelerator key="d" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_uncheck_all"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">uncheck_all_action</property> + <accelerator key="l" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menu_select_sep"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_select_all_without_job_code"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">Select All Without _Job Code</property> + <property name="use_underline">True</property> + <accelerator key="j" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_select_all_with_job_code"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">Select All Wit_h Job Code</property> + <property name="use_underline">True</property> + <accelerator key="h" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menuitem10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="menuitem10_menu"> + <property name="can_focus">False</property> + <property name="ubuntu_local">True</property> + <child> + <object class="GtkImageMenuItem" id="menu_zoom_in"> + <property name="label">gtk-zoom-in</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="plus" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <accelerator key="equal" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menu_zoom_out"> + <property name="label">gtk-zoom-out</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="seperator20"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="menu_log_window"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Error Log</property> + <property name="use_underline">True</property> + <signal name="toggled" handler="on_menu_log_window_toggled" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_clear"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Clear Completed Downloads</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="previous_image_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">prev_image_action</property> + <accelerator key="bracketleft" signal="activate"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="next_file_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">next_image_action</property> + <accelerator key="bracketright" signal="activate"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="help_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="help_menu"> + <property name="can_focus">False</property> + <property name="ubuntu_local">True</property> + <child> + <object class="GtkImageMenuItem" id="menu_get_help_online"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">get_help_action</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F1" signal="activate"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_report_problem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">report_problem_action</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_donate"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">donate_action</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Make a Donation...</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_translate"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">translate_action</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Translate this Application...</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menu_about"> + <property name="label">gtk-about</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="related_action">about_action</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVPaned" id="main_vpaned"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="position">1</property> + <property name="position_set">True</property> + <child> + <object class="GtkScrolledWindow" id="device_collection_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="device_collection_viewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="thumbnail_pane_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkNotebook" id="main_notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_tabs">False</property> + <property name="show_border">False</property> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow" id="thumbnails_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="thumbnail_control_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkButton" id="preview_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">show_image_action</property> + <property name="relief">none</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="check_all_button"> + <property name="label" translatable="yes">_Check All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">check_all_action</property> + <property name="relief">none</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="uncheck_all_button"> + <property name="label" translatable="yes">_Uncheck All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">uncheck_all_action</property> + <property name="relief">none</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">4</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 1</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEventBox" id="preview_eventbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <signal name="button-press-event" handler="on_preview_eventbox_button_press_event" swapped="no"/> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkHBox" id="image_controls_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkButton" id="next_image_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">next_image_action</property> + <property name="use_action_appearance">False</property> + <property name="relief">none</property> + <property name="focus_on_click">False</property> + <accelerator key="bracketright" signal="activate"/> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="prev_image_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">prev_image_action</property> + <property name="use_action_appearance">False</property> + <property name="relief">none</property> + <property name="focus_on_click">False</property> + <accelerator key="bracketleft" signal="activate"/> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">6</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButton" id="thumbnails_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">show_thumbnails_action</property> + <property name="relief">none</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">4</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="download_this_checkbutton"> + <property name="label" translatable="yes">_Include in download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <accelerator key="i" signal="activate"/> + <signal name="toggled" handler="on_download_this_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 2</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 3</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="buttons_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHButtonBox" id="main_buttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">edge</property> + <child> + <object class="GtkButton" id="help_button"> + <property name="label">_Help</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="related_action">help_action</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="download_button"> + <property name="label" translatable="yes">_Download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="related_action">download_action</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">6</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">6</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="statusbar_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="download_progressbar_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkProgressBar" id="download_progressbar"> + <property name="width_request">0</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="rapid_statusbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <property name="has_resize_grip">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkVSeparator" id="vseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHSeparator" id="hseparator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="speed_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label"> </property> + <property name="width_chars">9</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEventBox" id="error_eventbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <signal name="button-press-event" handler="on_error_eventbox_button_press_event" swapped="no"/> + <child> + <object class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkVSeparator" id="warning_vseparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkImage" id="error_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xpad">3</property> + <property name="stock">gtk-dialog-error</property> + <property name="icon-size">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkImage" id="warning_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="xpad">3</property> + <property name="stock">gtk-dialog-warning</property> + <property name="icon-size">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkVSeparator" id="vseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="statusbar1"> + <property name="width_request">15</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/rapid/glade3/thumbnails_icon.png b/rapid/glade3/thumbnails_icon.png Binary files differnew file mode 100644 index 0000000..d76d954 --- /dev/null +++ b/rapid/glade3/thumbnails_icon.png diff --git a/rapid/glade3/video.png b/rapid/glade3/video.png Binary files differdeleted file mode 100644 index 2dfc666..0000000 --- a/rapid/glade3/video.png +++ /dev/null diff --git a/rapid/glade3/video.svg b/rapid/glade3/video.svg new file mode 100644 index 0000000..0817d62 --- /dev/null +++ b/rapid/glade3/video.svg @@ -0,0 +1,956 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + width="96" + height="96" + id="svg2408" + inkscape:version="0.48.1 r9760" + sodipodi:docname="video.svg" + inkscape:export-filename="/home/damon/rapid/rapid/glade3/video66.png" + inkscape:export-xdpi="61.880001" + inkscape:export-ydpi="61.880001"> + <sodipodi:namedview + pagecolor="#444444" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="1" + inkscape:pageshadow="2" + inkscape:window-width="950" + inkscape:window-height="950" + id="namedview3275" + showgrid="false" + inkscape:zoom="3.0104167" + inkscape:cx="48" + inkscape:cy="48" + inkscape:window-x="93" + inkscape:window-y="58" + inkscape:window-maximized="0" + inkscape:current-layer="svg2408" /> + <defs + id="defs2410"> + <linearGradient + id="linearGradient4314"> + <stop + id="stop4316" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop4322" + style="stop-color:#ffffff;stop-opacity:0.49803922" + offset="0.30000001" /> + <stop + id="stop4318" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="45.447727" + y1="92.539597" + x2="45.447727" + y2="7.0165396" + id="ButtonShadow" + gradientUnits="userSpaceOnUse" + gradientTransform="scale(1.0058652,0.994169)"> + <stop + id="stop3750" + style="stop-color:#000000;stop-opacity:1" + offset="0" /> + <stop + id="stop3752" + style="stop-color:#000000;stop-opacity:0.58823532" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3737"> + <stop + id="stop3739" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3741" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <filter + color-interpolation-filters="sRGB" + id="filter3174"> + <feGaussianBlur + id="feGaussianBlur3176" + stdDeviation="1.71" /> + </filter> + <linearGradient + x1="36.357143" + y1="6" + x2="36.357143" + y2="63.893143" + id="linearGradient3188" + xlink:href="#linearGradient3737" + gradientUnits="userSpaceOnUse" /> + <filter + x="-0.192" + y="-0.192" + width="1.3839999" + height="1.3839999" + color-interpolation-filters="sRGB" + id="filter3794"> + <feGaussianBlur + id="feGaussianBlur3796" + stdDeviation="5.28" /> + </filter> + <linearGradient + x1="48" + y1="20.220806" + x2="48" + y2="138.66119" + id="linearGradient3613" + xlink:href="#linearGradient3737" + gradientUnits="userSpaceOnUse" /> + <radialGradient + cx="48" + cy="90.171875" + r="42" + fx="48" + fy="90.171875" + id="radialGradient3619" + xlink:href="#linearGradient3737" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.5510206,0.19713193)" /> + <clipPath + id="clipPath3613"> + <rect + width="84" + height="84" + rx="6" + ry="6" + x="6" + y="6" + id="rect3615" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </clipPath> + <linearGradient + x1="48" + y1="90" + x2="48" + y2="5.9877172" + id="linearGradient3617" + xlink:href="#linearGradient3187" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3187"> + <stop + id="stop3189" + style="stop-color:#282828;stop-opacity:1" + offset="0" /> + <stop + id="stop3191" + style="stop-color:#5a5a5a;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3386"> + <stop + id="stop3388" + style="stop-color:#eeeeec;stop-opacity:1" + offset="0" /> + <stop + id="stop3390" + style="stop-color:#eeeeec;stop-opacity:0" + offset="1" /> + </linearGradient> + <radialGradient + cx="53.419292" + cy="66.597893" + r="47.724609" + fx="53.419292" + fy="66.597893" + id="radialGradient3904" + xlink:href="#linearGradient3386" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0164118,0,0,1.2415079,-19.337623,-31.113008)" /> + <radialGradient + cx="53.419292" + cy="66.597893" + r="47.724609" + fx="53.419292" + fy="66.597893" + id="radialGradient3929" + xlink:href="#linearGradient3386" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0220726,0,0,1.2415079,-19.896446,-47.863009)" /> + <radialGradient + cx="53.419292" + cy="66.597893" + r="47.724609" + fx="53.419292" + fy="66.597893" + id="radialGradient3933" + xlink:href="#linearGradient3386" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.4750367,0,0,0.847859,-55.736923,-9.8942437)" /> + <radialGradient + cx="53.419292" + cy="66.597893" + r="47.724609" + fx="53.419292" + fy="66.597893" + id="radialGradient3937" + xlink:href="#linearGradient3386" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.4750367,0,0,0.847859,-77.736923,-9.8942437)" /> + <radialGradient + cx="53.419292" + cy="66.597893" + r="47.724609" + fx="53.419292" + fy="66.597893" + id="radialGradient4114" + xlink:href="#linearGradient3386" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0199392,0,0,0.85846172,-19.873341,-11.926904)" /> + <radialGradient + cx="53.419292" + cy="66.597893" + r="47.724609" + fx="53.419292" + fy="66.597893" + id="radialGradient4116" + xlink:href="#linearGradient3386" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0199392,0,0,0.85846172,-19.685841,-9.268213)" /> + <linearGradient + id="linearGradient3299"> + <stop + id="stop3301" + style="stop-color:#dfdfdf;stop-opacity:1" + offset="0" /> + <stop + id="stop3303" + style="stop-color:#434343;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3273"> + <stop + id="stop3275" + style="stop-color:#5d5d5d;stop-opacity:1" + offset="0" /> + <stop + id="stop3277" + style="stop-color:#171717;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3289"> + <stop + id="stop3291" + style="stop-color:#919191;stop-opacity:1" + offset="0" /> + <stop + id="stop3293" + style="stop-color:#232323;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3260" + gradientUnits="userSpaceOnUse"> + <stop + id="stop3262" + style="stop-color:#000000;stop-opacity:1" + offset="0" /> + <stop + id="stop3266" + style="stop-color:#000000;stop-opacity:0.3548387" + offset="0.2" /> + <stop + id="stop3268" + style="stop-color:#000000;stop-opacity:0" + offset="0.55254942" /> + <stop + id="stop3264" + style="stop-color:#000000;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="43.476658" + y1="26" + x2="43.476658" + y2="34.001266" + id="linearGradient4262" + xlink:href="#linearGradient3260" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.72413795,0,0,0.75,1.6551725,4.5)" /> + <linearGradient + x1="48" + y1="16" + x2="48" + y2="11.924657" + id="linearGradient4310" + xlink:href="#linearGradient3260" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,1.25,0,-4.5982122)" /> + <linearGradient + x1="50.464287" + y1="16.035713" + x2="50.464287" + y2="20.329885" + id="linearGradient4320" + xlink:href="#linearGradient4314" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,-0.67856979)" /> + <linearGradient + x1="48" + y1="16" + x2="48" + y2="11.924657" + id="linearGradient4326" + xlink:href="#linearGradient3260" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,1.25,0,4.4017878)" /> + <linearGradient + x1="11" + y1="10.441942" + x2="16" + y2="24" + id="linearGradient4371" + xlink:href="#linearGradient3273" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.81330712,0,0,0.81132563,3.9935431,2.0281849)" /> + <linearGradient + x1="13.125" + y1="9.9419422" + x2="19.25" + y2="24.5" + id="linearGradient4373" + xlink:href="#linearGradient3289" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.81330712,0,0,0.81132563,3.9935431,2.0281849)" /> + <radialGradient + cx="11.3125" + cy="14.9375" + r="1.4375" + fx="11.3125" + fy="14.9375" + id="radialGradient4375" + xlink:href="#linearGradient3299" + gradientUnits="userSpaceOnUse" /> + <radialGradient + cx="11.3125" + cy="14.9375" + r="1.4375" + fx="11.3125" + fy="14.9375" + id="radialGradient4377" + xlink:href="#linearGradient3299" + gradientUnits="userSpaceOnUse" /> + <radialGradient + cx="11.3125" + cy="14.9375" + r="1.4375" + fx="11.3125" + fy="14.9375" + id="radialGradient4379" + xlink:href="#linearGradient3299" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3277"> + <stop + id="stop3279" + style="stop-color:#3d9119;stop-opacity:1" + offset="0" /> + <stop + id="stop3281" + style="stop-color:#76e62b;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient3266"> + <stop + id="stop3269" + style="stop-color:#87ff2b;stop-opacity:1" + offset="0" /> + <stop + id="stop3271" + style="stop-color:#87ff2b;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="70.782135" + y1="83.017471" + x2="75.404488" + y2="22.02951" + id="linearGradient4520" + xlink:href="#linearGradient3277" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0513134,0,0,1.0513134,-120.2285,31.416155)" /> + <radialGradient + cx="40.846539" + cy="75.263962" + r="14.672466" + fx="40.846539" + fy="75.263962" + id="radialGradient4522" + xlink:href="#linearGradient3266" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9847961,0.02597927,-0.01823822,0.6913564,1.9937062,24.820256)" /> + <radialGradient + cx="40.846539" + cy="75.263962" + r="14.672466" + fx="40.846539" + fy="75.263962" + id="radialGradient4524" + xlink:href="#linearGradient3266" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9847961,0.02597927,-0.01823822,0.6913564,1.9937062,24.820256)" /> + <radialGradient + cx="54.75" + cy="25.072803" + r="13" + fx="54.75" + fy="25.072803" + id="radialGradient4526" + xlink:href="#linearGradient3266" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.543369,0.28142051,-0.32722971,1.7945958,-138.17549,-6.0460513)" /> + <radialGradient + cx="56.496048" + cy="31.701607" + r="13" + fx="56.496048" + fy="31.701607" + id="radialGradient4528" + xlink:href="#linearGradient3266" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.88267832,0,-2.2566243e-8,1.115007,-69.080446,51.097006)" /> + <clipPath + id="clipPath3754"> + <rect + width="96" + height="96" + x="-100" + y="-3.469447e-15" + id="rect3756" + style="fill:#ffffff;fill-opacity:1;stroke:none" /> + </clipPath> + <linearGradient + x1="45.447727" + y1="92.539597" + x2="45.447727" + y2="7.0165396" + id="ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0058652,0,0,0.994169,100,0)"> + <stop + id="stop3750-8" + style="stop-color:#000000;stop-opacity:1" + offset="0" /> + <stop + id="stop3752-5" + style="stop-color:#000000;stop-opacity:0.58823532" + offset="1" /> + </linearGradient> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3780" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3772" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3725" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3721" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,-97)" /> + <linearGradient + x1="32.251034" + y1="6.1317081" + x2="32.251034" + y2="90.238609" + id="linearGradient3811" + xlink:href="#ButtonShadow-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3277" + id="linearGradient3157" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0513134,0,0,1.0513134,-120.2285,31.416155)" + x1="70.782135" + y1="83.017471" + x2="75.404488" + y2="22.02951" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266" + id="radialGradient3159" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9847961,0.02597927,-0.01823822,0.6913564,1.9937062,24.820256)" + cx="40.846539" + cy="75.263962" + fx="40.846539" + fy="75.263962" + r="14.672466" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266" + id="radialGradient3161" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9847961,0.02597927,-0.01823822,0.6913564,1.9937062,24.820256)" + cx="40.846539" + cy="75.263962" + fx="40.846539" + fy="75.263962" + r="14.672466" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266" + id="radialGradient3163" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.543369,0.28142051,-0.32722971,1.7945958,-138.17549,-6.0460513)" + cx="54.75" + cy="25.072803" + fx="54.75" + fy="25.072803" + r="13" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266" + id="radialGradient3165" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.88267832,0,-2.2566243e-8,1.115007,-69.080446,51.097006)" + cx="56.496048" + cy="31.701607" + fx="56.496048" + fy="31.701607" + r="13" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3277-7" + id="linearGradient3157-9" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0513134,0,0,1.0513134,-120.2285,31.416155)" + x1="70.782135" + y1="83.017471" + x2="75.404488" + y2="22.02951" /> + <linearGradient + id="linearGradient3277-7"> + <stop + id="stop3279-5" + style="stop-color:#3d9119;stop-opacity:1" + offset="0" /> + <stop + id="stop3281-9" + style="stop-color:#76e62b;stop-opacity:1" + offset="1" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266-8" + id="radialGradient3159-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9847961,0.02597927,-0.01823822,0.6913564,1.9937062,24.820256)" + cx="40.846539" + cy="75.263962" + fx="40.846539" + fy="75.263962" + r="14.672466" /> + <linearGradient + id="linearGradient3266-8"> + <stop + id="stop3269-5" + style="stop-color:#87ff2b;stop-opacity:1" + offset="0" /> + <stop + id="stop3271-3" + style="stop-color:#87ff2b;stop-opacity:0" + offset="1" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266-8" + id="radialGradient3161-3" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9847961,0.02597927,-0.01823822,0.6913564,1.9937062,24.820256)" + cx="40.846539" + cy="75.263962" + fx="40.846539" + fy="75.263962" + r="14.672466" /> + <linearGradient + id="linearGradient3196"> + <stop + id="stop3198" + style="stop-color:#87ff2b;stop-opacity:1" + offset="0" /> + <stop + id="stop3200" + style="stop-color:#87ff2b;stop-opacity:0" + offset="1" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266-8" + id="radialGradient3163-8" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.543369,0.28142051,-0.32722971,1.7945958,-138.17549,-6.0460513)" + cx="54.75" + cy="25.072803" + fx="54.75" + fy="25.072803" + r="13" /> + <linearGradient + id="linearGradient3203"> + <stop + id="stop3205" + style="stop-color:#87ff2b;stop-opacity:1" + offset="0" /> + <stop + id="stop3207" + style="stop-color:#87ff2b;stop-opacity:0" + offset="1" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3266-8" + id="radialGradient3165-3" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.88267832,0,-2.2566243e-8,1.115007,-69.080446,51.097006)" + cx="56.496048" + cy="31.701607" + fx="56.496048" + fy="31.701607" + r="13" /> + <linearGradient + id="linearGradient3210"> + <stop + id="stop3212" + style="stop-color:#87ff2b;stop-opacity:1" + offset="0" /> + <stop + id="stop3214" + style="stop-color:#87ff2b;stop-opacity:0" + offset="1" /> + </linearGradient> + <radialGradient + r="13" + fy="31.701607" + fx="56.496048" + cy="31.701607" + cx="56.496048" + gradientTransform="matrix(0.88267832,0,-2.2566243e-8,1.115007,-69.080446,51.097006)" + gradientUnits="userSpaceOnUse" + id="radialGradient3222" + xlink:href="#linearGradient3266-8" + inkscape:collect="always" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3386" + id="radialGradient3284" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.4750367,0,0,0.847859,-77.736923,-9.8942437)" + cx="53.419292" + cy="66.597893" + fx="53.419292" + fy="66.597893" + r="47.724609" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3386" + id="radialGradient3287" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0199392,0,0,0.85846172,-19.873341,-11.926904)" + cx="53.419292" + cy="66.597893" + fx="53.419292" + fy="66.597893" + r="47.724609" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3386" + id="radialGradient3290" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.4750367,0,0,0.847859,-55.736923,-9.8942437)" + cx="53.419292" + cy="66.597893" + fx="53.419292" + fy="66.597893" + r="47.724609" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3386" + id="radialGradient3293" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0220726,0,0,1.2415079,-19.896446,-47.863009)" + cx="53.419292" + cy="66.597893" + fx="53.419292" + fy="66.597893" + r="47.724609" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3386" + id="radialGradient3296" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0164118,0,0,1.2415079,-19.337623,-31.113008)" + cx="53.419292" + cy="66.597893" + fx="53.419292" + fy="66.597893" + r="47.724609" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3386" + id="radialGradient3299" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0199392,0,0,0.85846172,-19.685841,-9.268213)" + cx="53.419292" + cy="66.597893" + fx="53.419292" + fy="66.597893" + r="47.724609" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3187" + id="linearGradient3302" + gradientUnits="userSpaceOnUse" + x1="48" + y1="90" + x2="48" + y2="5.9877172" /> + </defs> + <metadata + id="metadata2413"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="layer2" + style="display:none"> + <rect + width="86" + height="85" + rx="6" + ry="6" + x="5" + y="7" + id="rect3745" + style="opacity:0.9;fill:url(#ButtonShadow);fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3174)" /> + </g> + <rect + style="fill:url(#linearGradient3302);fill-opacity:1;fill-rule:nonzero;stroke:none" + id="rect2419" + y="6" + x="6" + ry="6" + rx="6" + height="84" + width="84" /> + <path + style="font-size:6px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.8;fill:url(#radialGradient3299);fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans Bold" + id="path3890" + inkscape:connector-curvature="0" + d="m 36.5625,32.0625 c -0.319303,3e-6 -0.56595,0.04413 -0.75,0.125 -0.182658,0.07948 -0.31423,0.171675 -0.40625,0.3125 -0.09063,0.139436 -0.15625,0.311193 -0.15625,0.46875 0,0.239827 0.102775,0.407731 0.28125,0.5625 0.177079,0.154772 0.459488,0.28158 0.875,0.375 0.253766,0.05577 0.43028,0.12476 0.5,0.1875 C 36.97597,34.1565 37,34.23302 37,34.3125 c -2e-6,0.08366 -0.01985,0.15461 -0.09375,0.21875 -0.07251,0.06275 -0.177251,0.09375 -0.3125,0.09375 -0.181264,10e-7 -0.3399,-0.0634 -0.4375,-0.1875 -0.05996,-0.07669 -0.10548,-0.197344 -0.125,-0.34375 l -0.875,0.0625 c 0.02649,0.309543 0.111716,0.580466 0.3125,0.78125 0.200783,0.200784 0.572269,0.28125 1.09375,0.28125 0.296991,0 0.554791,-0.03995 0.75,-0.125 0.195204,-0.08645 0.32874,-0.209074 0.4375,-0.375 0.108757,-0.165925 0.187497,-0.334648 0.1875,-0.53125 -3e-6,-0.167318 -0.04274,-0.333498 -0.125,-0.46875 -0.08087,-0.135248 -0.226384,-0.25312 -0.40625,-0.34375 -0.178477,-0.09202 -0.460885,-0.16076 -0.875,-0.25 C 36.363929,33.09015 36.26337,33.04044 36.21875,33 36.17274,32.96097 36.125,32.9238 36.125,32.875 c 0,-0.06693 0.03798,-0.14148 0.09375,-0.1875 0.05577,-0.0474 0.139846,-0.0625 0.25,-0.0625 0.133854,0 0.2372,0.03101 0.3125,0.09375 0.07669,0.06275 0.13254,0.174464 0.15625,0.3125 l 0.875,-0.0625 C 37.77346,32.650845 37.675108,32.426263 37.46875,32.28125 37.263781,32.134852 36.954306,32.062503 36.5625,32.0625 z m 14.96875,0 c -0.497778,3e-6 -0.877385,0.127386 -1.15625,0.40625 -0.278867,0.278869 -0.4375,0.686937 -0.4375,1.1875 0,0.358345 0.07792,0.63657 0.21875,0.875 0.140827,0.238431 0.336618,0.421098 0.5625,0.53125 0.227275,0.110148 0.497954,0.15625 0.84375,0.15625 0.340215,0 0.616471,-0.06062 0.84375,-0.1875 0.228667,-0.128278 0.411335,-0.302579 0.53125,-0.53125 0.121301,-0.230064 0.187496,-0.516655 0.1875,-0.875 -4e-6,-0.493591 -0.130175,-0.882958 -0.40625,-1.15625 -0.27608,-0.27468 -0.677177,-0.406247 -1.1875,-0.40625 z m 3.375,0 c -0.319304,3e-6 -0.597199,0.04413 -0.78125,0.125 -0.182658,0.07948 -0.31422,0.171675 -0.40625,0.3125 -0.09063,0.139436 -0.125,0.311193 -0.125,0.46875 0,0.239827 0.07153,0.407731 0.25,0.5625 0.17708,0.154772 0.490738,0.28158 0.90625,0.375 0.253767,0.05577 0.39903,0.12476 0.46875,0.1875 0.06972,0.06275 0.124998,0.13927 0.125,0.21875 -2e-6,0.08366 -0.0511,0.15461 -0.125,0.21875 C 55.14624,34.594 55.041498,34.625 54.90625,34.625 54.724985,34.625001 54.5976,34.5616 54.5,34.4375 54.44004,34.36081 54.39452,34.240156 54.375,34.09375 l -0.90625,0.0625 c 0.02649,0.309543 0.142966,0.580466 0.34375,0.78125 0.200783,0.200784 0.572269,0.28125 1.09375,0.28125 0.296991,0 0.523541,-0.03995 0.71875,-0.125 0.195204,-0.08645 0.359989,-0.209074 0.46875,-0.375 0.108756,-0.165925 0.156247,-0.334648 0.15625,-0.53125 -3e-6,-0.167318 -0.04274,-0.333498 -0.125,-0.46875 C 56.04413,33.583502 55.929866,33.46563 55.75,33.375 55.571523,33.28298 55.257865,33.21424 54.84375,33.125 54.676429,33.09015 54.57587,33.04044 54.53125,33 c -0.04602,-0.03903 -0.0625,-0.0762 -0.0625,-0.125 0,-0.06693 0.03798,-0.14148 0.09375,-0.1875 0.05577,-0.0474 0.139847,-0.0625 0.25,-0.0625 0.133855,0 0.2372,0.03101 0.3125,0.09375 0.07669,0.06275 0.10129,0.174464 0.125,0.3125 l 0.90625,-0.0625 c -0.03905,-0.317905 -0.168642,-0.542487 -0.375,-0.6875 -0.204969,-0.146398 -0.483195,-0.218747 -0.875,-0.21875 z M 15,32.125 l 0,3.0625 0.9375,0 0,-1.25 1.1875,0 0,-0.625 -1.1875,0 0,-0.53125 1.40625,0 0,-0.65625 -2.34375,0 z m 3.3125,0 -1.15625,3.0625 0.96875,0 0.125,-0.53125 1.09375,0 0.15625,0.53125 0.96875,0 -1.125,-3.0625 -1.03125,0 z m 2.46875,0 0,3.0625 2.59375,0 0,-0.71875 -1.65625,0 0,-0.59375 1.5,0 0,-0.625 -1.5,0 0,-0.5 1.59375,0 0,-0.625 -2.53125,0 z m 3.09375,0 0,3.0625 0.90625,0 0,-1.6875 1.15625,1.6875 0.875,0 0,-3.0625 -0.875,0 0,1.6875 -1.15625,-1.6875 -0.90625,0 z m 3.5,0 0,0.625 1.53125,0 -1.6875,1.78125 0,0.65625 2.90625,0 0,-0.65625 -1.78125,0 1.71875,-1.8125 0,-0.59375 -2.6875,0 z m 4.03125,0 -1.15625,3.0625 0.96875,0 0.125,-0.53125 1.09375,0 0.15625,0.53125 0.96875,0 -1.125,-3.0625 -1.03125,0 z m 6.78125,0 0,0.75 0.96875,0 0,2.3125 0.9375,0 0,-2.3125 0.96875,0 0,-0.75 -2.875,0 z m 3.3125,0 0,1.8125 c 0,0.150589 0.03519,0.336044 0.09375,0.53125 0.03625,0.121307 0.0899,0.229415 0.1875,0.34375 0.099,0.114335 0.225231,0.18726 0.34375,0.25 0.118517,0.06135 0.261813,0.10409 0.4375,0.125 0.177079,0.02091 0.350804,0.03125 0.5,0.03125 0.257949,0 0.472195,-0.02543 0.65625,-0.09375 0.132459,-0.0488 0.255084,-0.130087 0.375,-0.25 0.121304,-0.121306 0.19422,-0.277152 0.25,-0.4375 0.05716,-0.161742 0.09375,-0.318736 0.09375,-0.5 l 0,-1.8125 -0.9375,0 0,1.84375 c -2e-6,0.170109 -0.06283,0.31283 -0.15625,0.40625 -0.09203,0.09203 -0.211865,0.125 -0.375,0.125 -0.164533,0 -0.28158,-0.03158 -0.375,-0.125 -0.09203,-0.09481 -0.156251,-0.23893 -0.15625,-0.40625 l 0,-1.84375 -0.9375,0 z m 3.59375,0 0,3.0625 1.40625,0 c 0.168713,0 0.354742,-0.03798 0.5625,-0.09375 0.15198,-0.04044 0.273786,-0.128693 0.40625,-0.25 0.132459,-0.1227 0.2386,-0.25763 0.3125,-0.4375 0.07529,-0.181262 0.124997,-0.45358 0.125,-0.78125 0,-0.209148 -0.01231,-0.406908 -0.0625,-0.59375 -0.0502,-0.186838 -0.138456,-0.35917 -0.25,-0.5 -0.111549,-0.140825 -0.266,-0.2372 -0.4375,-0.3125 C 46.986139,32.143459 46.77747,32.125003 46.5,32.125 l -1.40625,0 z m 3.34375,0 0,3.0625 0.9375,0 0,-3.0625 -0.9375,0 z m 3.09375,0.65625 c 0.199387,3e-6 0.353018,0.05365 0.46875,0.1875 0.117126,0.132464 0.187498,0.336376 0.1875,0.625 -2e-6,0.343007 -0.07596,0.58629 -0.1875,0.71875 -0.111549,0.132463 -0.26518,0.21875 -0.46875,0.21875 -0.197997,0 -0.353022,-0.0835 -0.46875,-0.21875 -0.114333,-0.135249 -0.156251,-0.346707 -0.15625,-0.65625 -10e-7,-0.312328 0.04052,-0.552247 0.15625,-0.6875 0.115728,-0.135245 0.277725,-0.187497 0.46875,-0.1875 z m -5.5,0.03125 0.25,0 c 0.245401,3e-6 0.394028,0.03634 0.5,0.15625 0.105963,0.119915 0.187498,0.362622 0.1875,0.6875 0,0.245404 -0.04634,0.421099 -0.09375,0.53125 -0.04741,0.108759 -0.10384,0.17413 -0.1875,0.21875 -0.08366,0.04323 -0.239507,0.0625 -0.4375,0.0625 l -0.21875,0 0,-1.65625 z M 18.8125,32.90625 19.15625,34 l -0.6875,0 0.34375,-1.09375 z m 13.09375,0 L 32.25,34 l -0.6875,0 0.34375,-1.09375 z m -8.875,4.96875 c -0.364643,2e-6 -0.649275,0.05669 -0.84375,0.21875 -0.194472,0.162065 -0.3125,0.417587 -0.3125,0.71875 0,0.240394 0.07964,0.420698 0.21875,0.5625 0.140453,0.141807 0.353732,0.24767 0.65625,0.3125 l 0.3125,0.0625 c 0.183669,0.04051 0.31423,0.07369 0.375,0.125 0.06213,0.05132 0.09375,0.12421 0.09375,0.21875 -2e-6,0.105341 -0.06576,0.19328 -0.15625,0.25 -0.09049,0.05671 -0.204836,0.09375 -0.375,0.09375 -0.167465,0 -0.350282,-0.04243 -0.53125,-0.09375 -0.179621,-0.05267 -0.369375,-0.116109 -0.5625,-0.21875 l 0,0.65625 C 22.099375,40.85283 22.306875,40.90104 22.5,40.9375 22.693123,40.97397 22.870724,41 23.0625,41 c 0.406505,0 0.70907,-0.08794 0.90625,-0.25 0.19852,-0.163412 0.312497,-0.41642 0.3125,-0.75 -3e-6,-0.253896 -0.07695,-0.424746 -0.21875,-0.5625 -0.141807,-0.137752 -0.382472,-0.24497 -0.71875,-0.3125 L 23,39.0625 c -0.158012,-0.03241 -0.25173,-0.08043 -0.3125,-0.125 -0.05942,-0.04592 -0.09375,-0.10647 -0.09375,-0.1875 0,-0.10804 0.03586,-0.19869 0.125,-0.25 0.08913,-0.05131 0.219877,-0.0625 0.40625,-0.0625 0.140453,0 0.310736,-0.0065 0.46875,0.03125 0.158008,0.03782 0.302633,0.11322 0.46875,0.1875 l 0,-0.65625 c -0.187725,-0.04997 -0.355685,-0.06943 -0.53125,-0.09375 -0.17557,-0.02566 -0.333888,-0.03125 -0.5,-0.03125 z m 2.78125,0 c -0.364642,3e-6 -0.649275,0.05669 -0.84375,0.21875 -0.194472,0.162065 -0.281251,0.417587 -0.28125,0.71875 -1e-6,0.240395 0.07965,0.420697 0.21875,0.5625 0.140453,0.141806 0.353733,0.24767 0.65625,0.3125 L 25.875,39.75 c 0.18367,0.04051 0.28298,0.07368 0.34375,0.125 0.06212,0.05132 0.09375,0.12421 0.09375,0.21875 -1e-6,0.105342 -0.03451,0.19329 -0.125,0.25 -0.09049,0.05671 -0.204836,0.09375 -0.375,0.09375 -0.167467,0 -0.350282,-0.04243 -0.53125,-0.09375 -0.17962,-0.05267 -0.369374,-0.11611 -0.5625,-0.21875 l 0,0.65625 c 0.193126,0.07158 0.369375,0.11979 0.5625,0.15625 C 25.474373,40.97397 25.683224,41 25.875,41 c 0.406505,0 0.709071,-0.08794 0.90625,-0.25 0.19852,-0.163412 0.281248,-0.416421 0.28125,-0.75 -2e-6,-0.253897 -0.0457,-0.424746 -0.1875,-0.5625 -0.141807,-0.137752 -0.382472,-0.24497 -0.71875,-0.3125 L 25.8125,39.0625 C 25.654489,39.03009 25.56077,38.98207 25.5,38.9375 25.44058,38.89158 25.40625,38.83103 25.40625,38.75 c -2e-6,-0.10804 0.03586,-0.19869 0.125,-0.25 0.08913,-0.05131 0.219876,-0.0625 0.40625,-0.0625 0.140453,0 0.310737,-0.0066 0.46875,0.03125 0.158009,0.03782 0.302634,0.11322 0.46875,0.1875 L 26.875,38 C 26.687275,37.950031 26.519315,37.93057 26.34375,37.90625 26.168181,37.88059 25.978613,37.875003 25.8125,37.875 z m 6.28125,0 c -0.349448,0.05242 -0.618889,0.196585 -0.84375,0.40625 -0.299816,0.279562 -0.4375,0.668714 -0.4375,1.15625 0,0.482137 0.143086,0.873992 0.4375,1.15625 C 31.544413,40.874658 31.933755,41 32.4375,41 c 0.222834,0 0.446916,-0.01524 0.65625,-0.0625 0.209328,-0.04727 0.399272,-0.12421 0.59375,-0.21875 l 0,-1.46875 -1.21875,0 0,0.53125 0.46875,0 0,0.59375 c -0.05537,0.0216 -0.11727,0.02045 -0.1875,0.03125 -0.06888,0.0095 -0.13772,0.03125 -0.21875,0.03125 -0.298467,10e-7 -0.525439,-0.07578 -0.6875,-0.25 -0.162064,-0.174215 -0.250001,-0.429925 -0.25,-0.75 -1e-6,-0.322772 0.08254,-0.577132 0.25,-0.75 0.168814,-0.174216 0.408129,-0.249998 0.71875,-0.25 0.167463,0 0.333884,0.01524 0.5,0.0625 0.167462,0.04727 0.362433,0.12286 0.53125,0.21875 l 0,-0.625 C 33.430335,38.01947 33.247518,37.94406 33.0625,37.90625 32.878826,37.86844 32.669977,37.875 32.46875,37.875 c -0.130326,10e-7 -0.258517,-0.01747 -0.375,0 z M 15,37.90625 l 0,3.03125 0.75,0 0,-2.21875 0.6875,1.65625 0.5,0 0.6875,-1.65625 0,2.21875 0.75,0 0,-3.03125 -1,0 -0.6875,1.625 -0.6875,-1.625 -1,0 z m 4.125,0 0,3.03125 2.15625,0 0,-0.59375 -1.375,0 0,-0.6875 1.25,0 0,-0.59375 -1.25,0 0,-0.5625 1.34375,0 0,-0.59375 -2.125,0 z m 15.25,0 0,3.03125 2.15625,0 0,-0.59375 -1.375,0 0,-0.6875 1.25,0 0,-0.59375 -1.25,0 0,-0.5625 1.3125,0 0,-0.59375 -2.09375,0 z m -5.84375,0.03125 -1.125,3.03125 0.78125,0 0.1875,-0.5625 1.21875,0 0.1875,0.5625 0.78125,0 -1.125,-3.03125 -0.90625,0 z M 29,38.625 l 0.40625,1.21875 -0.84375,0 L 29,38.625 z" /> + <path + style="font-size:6px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.8;fill:url(#radialGradient3296);fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans Bold" + id="path3842" + inkscape:connector-curvature="0" + d="m 40,39 41,0 0,1 -41,0 0,-1 z" /> + <path + style="font-size:6px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.8;fill:url(#radialGradient3293);fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans Bold" + id="path3840" + inkscape:connector-curvature="0" + d="m 15,69 66,0 0,1 -66,0 0,-1 z" /> + <path + style="font-size:6px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.8;fill:url(#radialGradient3290);fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans Bold" + id="text3348" + inkscape:connector-curvature="0" + d="m 59,70 0,14 -1,0 0,-14 1,0 z" /> + <path + style="font-size:6px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.8;fill:url(#radialGradient3287);fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans Bold" + id="path3870" + inkscape:connector-curvature="0" + d="m 41.78125,62.875 c -0.364641,3e-6 -0.649276,0.05669 -0.84375,0.21875 -0.194473,0.162065 -0.28125,0.417586 -0.28125,0.71875 0,0.240395 0.07964,0.420696 0.21875,0.5625 0.140454,0.141805 0.353733,0.24767 0.65625,0.3125 l 0.3125,0.0625 c 0.183669,0.04051 0.28298,0.07368 0.34375,0.125 0.06212,0.05132 0.09375,0.12421 0.09375,0.21875 -2e-6,0.105341 -0.03452,0.19328 -0.125,0.25 -0.09049,0.05671 -0.204835,0.09375 -0.375,0.09375 -0.167466,0 -0.350281,-0.04243 -0.53125,-0.09375 -0.17962,-0.05267 -0.369376,-0.11611 -0.5625,-0.21875 l 0,0.65625 c 0.193124,0.07157 0.369376,0.11979 0.5625,0.15625 0.193125,0.03647 0.401974,0.0625 0.59375,0.0625 0.406504,0 0.709071,-0.08794 0.90625,-0.25 0.19852,-0.163413 0.281247,-0.41642 0.28125,-0.75 -3e-6,-0.253896 -0.0457,-0.424745 -0.1875,-0.5625 C 42.701942,64.299749 42.461277,64.19253 42.125,64.125 L 41.78125,64.0625 C 41.623237,64.03009 41.52952,63.98207 41.46875,63.9375 41.40933,63.891583 41.375,63.83103 41.375,63.75 c 0,-0.10804 0.03586,-0.19868 0.125,-0.25 0.08913,-0.05131 0.219875,-0.0625 0.40625,-0.0625 0.140451,0 0.310736,-0.0066 0.46875,0.03125 0.158008,0.03782 0.302634,0.11322 0.46875,0.1875 l 0,-0.65625 C 42.656025,62.950032 42.488067,62.93057 42.3125,62.90625 42.136931,62.88059 41.947363,62.875003 41.78125,62.875 z m 3.0625,0 C 44.505191,62.92793 44.220809,63.069558 44,63.28125 c -0.294414,0.28091 -0.4375,0.672764 -0.4375,1.15625 0,0.482137 0.143086,0.873991 0.4375,1.15625 C 44.294412,65.874658 44.683754,66 45.1875,66 c 0.168813,0 0.344688,-0.02468 0.5,-0.0625 0.155308,-0.03781 0.294342,-0.08197 0.4375,-0.15625 l 0,-0.625 c -0.144508,0.09859 -0.265799,0.17283 -0.40625,0.21875 -0.140457,0.04592 -0.313442,0.0625 -0.46875,0.0625 -0.278209,10e-7 -0.49689,-0.102981 -0.65625,-0.28125 -0.159363,-0.178269 -0.218751,-0.406778 -0.21875,-0.71875 -10e-7,-0.313321 0.05939,-0.57173 0.21875,-0.75 0.15936,-0.178264 0.378041,-0.249998 0.65625,-0.25 0.155308,0 0.328293,0.01659 0.46875,0.0625 0.140451,0.04592 0.261742,0.12016 0.40625,0.21875 l 0,-0.625 c -0.143158,-0.07428 -0.282192,-0.14969 -0.4375,-0.1875 -0.155312,-0.0378 -0.331187,-0.03125 -0.5,-0.03125 -0.125937,10e-7 -0.230897,-0.01764 -0.34375,0 z m -18.875,0.03125 0,0.59375 1,0 0,2.4375 0.78125,0 0,-2.4375 1,0 0,-0.59375 -2.78125,0 z m 3.1875,0 0,3.03125 2.15625,0 0,-0.59375 -1.375,0 0,-0.6875 1.25,0 0,-0.59375 -1.25,0 0,-0.5625 1.3125,0 0,-0.59375 -2.09375,0 z m 17.625,0 0,3.03125 2.15625,0 0,-0.59375 -1.375,0 0,-0.6875 1.25,0 0,-0.59375 -1.25,0 0,-0.5625 1.3125,0 0,-0.59375 -2.09375,0 z m 2.84375,0 0,3.03125 0.71875,0 0,-2.0625 1.125,2.0625 0.84375,0 0,-3.03125 -0.71875,0 0,2.09375 -1.09375,-2.09375 -0.875,0 z m 3.46875,0 0,3.03125 2.15625,0 0,-0.59375 -1.375,0 0,-0.6875 1.25,0 0,-0.59375 -1.25,0 0,-0.5625 1.3125,0 0,-0.59375 -2.09375,0 z m 11.65625,0 0,0.59375 1,0 0,2.4375 0.78125,0 0,-2.4375 1,0 0,-0.59375 -2.78125,0 z m 6.09375,0 0,3.03125 0.78125,0 0,-1.1875 1.1875,1.1875 0.96875,0 -1.59375,-1.59375 1.4375,-1.4375 -0.90625,0 -1.09375,1.125 0,-1.125 -0.78125,0 z m 3.1875,0 0,3.03125 2.15625,0 0,-0.59375 -1.375,0 0,-0.6875 1.25,0 0,-0.59375 -1.25,0 0,-0.5625 1.34375,0 0,-0.59375 -2.125,0 z M 20,62.9375 l 0,3.03125 0.8125,0 c 0.453773,0 0.80436,-0.06017 1.03125,-0.125 0.226885,-0.06618 0.431684,-0.15584 0.59375,-0.3125 0.141802,-0.136402 0.24362,-0.290479 0.3125,-0.46875 0.06887,-0.179617 0.09375,-0.39676 0.09375,-0.625 -2e-6,-0.225535 -0.02487,-0.415479 -0.09375,-0.59375 C 22.68112,63.665484 22.579302,63.511405 22.4375,63.375 22.276784,63.218343 22.069286,63.09608 21.84375,63.03125 21.619563,62.96508 21.271675,62.9375 20.8125,62.9375 l -0.8125,0 z m 4.1875,0 -1.09375,3.03125 0.78125,0 0.1875,-0.5625 1.21875,0 0.1875,0.5625 0.78125,0 -1.125,-3.03125 -0.9375,0 z m 44.1875,0 -1.125,3.03125 0.78125,0 0.21875,-0.5625 1.21875,0 0.1875,0.5625 0.78125,0 -1.125,-3.03125 -0.9375,0 z m -47.59375,0.59375 0.28125,0 c 0.320071,2e-6 0.551284,0.06209 0.71875,0.21875 0.167457,0.156663 0.249997,0.387686 0.25,0.6875 -3e-6,0.301167 -0.08119,0.52949 -0.25,0.6875 -0.167467,0.158017 -0.400029,0.25 -0.71875,0.25 l -0.28125,0 0,-1.84375 z m 3.875,0.09375 0.4375,1.21875 -0.84375,0 0.40625,-1.21875 z m 44.1875,0 0.40625,1.21875 -0.8125,0 0.40625,-1.21875 z" /> + <path + style="font-size:6px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;opacity:0.8;fill:url(#radialGradient3284);fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans Bold" + id="path3935" + inkscape:connector-curvature="0" + d="m 37,70 0,14 -1,0 0,-14 1,0 z" /> + <g + id="layer5"> + <path + d="M 12,90 C 8.676,90 6,87.324 6,84 L 6,82 6,14 6,12 c 0,-0.334721 0.04135,-0.6507 0.09375,-0.96875 0.0487,-0.295596 0.09704,-0.596915 0.1875,-0.875 C 6.29113,10.12587 6.302142,10.09265 6.3125,10.0625 6.411365,9.774729 6.5473802,9.515048 6.6875,9.25 6.8320918,8.976493 7.0031161,8.714385 7.1875,8.46875 7.3718839,8.223115 7.5612765,7.995278 7.78125,7.78125 8.221197,7.353194 8.72416,6.966724 9.28125,6.6875 9.559795,6.547888 9.8547231,6.440553 10.15625,6.34375 9.9000482,6.443972 9.6695391,6.580022 9.4375,6.71875 c -0.00741,0.0044 -0.023866,-0.0045 -0.03125,0 -0.031933,0.0193 -0.062293,0.04251 -0.09375,0.0625 -0.120395,0.0767 -0.2310226,0.163513 -0.34375,0.25 -0.1061728,0.0808 -0.2132809,0.161112 -0.3125,0.25 C 8.4783201,7.442683 8.3087904,7.626638 8.15625,7.8125 8.0486711,7.942755 7.9378561,8.077785 7.84375,8.21875 7.818661,8.25713 7.805304,8.30462 7.78125,8.34375 7.716487,8.446782 7.6510225,8.548267 7.59375,8.65625 7.4927417,8.850956 7.3880752,9.071951 7.3125,9.28125 7.30454,9.30306 7.288911,9.3218 7.28125,9.34375 7.2494249,9.4357 7.2454455,9.530581 7.21875,9.625 7.1884177,9.731618 7.1483606,9.828031 7.125,9.9375 7.0521214,10.279012 7,10.635705 7,11 l 0,2 0,68 0,2 c 0,2.781848 2.2181517,5 5,5 l 2,0 68,0 2,0 c 2.781848,0 5,-2.218152 5,-5 l 0,-2 0,-68 0,-2 C 89,10.635705 88.94788,10.279012 88.875,9.9375 88.83085,9.730607 88.78662,9.539842 88.71875,9.34375 88.71105,9.3218 88.69545,9.30306 88.6875,9.28125 88.62476,9.107511 88.549117,8.913801 88.46875,8.75 88.42717,8.6672 88.38971,8.580046 88.34375,8.5 88.28915,8.40279 88.216976,8.31165 88.15625,8.21875 88.06214,8.077785 87.951329,7.942755 87.84375,7.8125 87.700576,7.63805 87.540609,7.465502 87.375,7.3125 87.36383,7.3023 87.35502,7.29135 87.34375,7.28125 87.205364,7.155694 87.058659,7.046814 86.90625,6.9375 86.803679,6.86435 86.701932,6.784136 86.59375,6.71875 c -0.0074,-0.0045 -0.02384,0.0044 -0.03125,0 -0.232039,-0.138728 -0.462548,-0.274778 -0.71875,-0.375 0.301527,0.0968 0.596455,0.204138 0.875,0.34375 0.55709,0.279224 1.060053,0.665694 1.5,1.09375 0.219973,0.214028 0.409366,0.441865 0.59375,0.6875 0.184384,0.245635 0.355408,0.507743 0.5,0.78125 0.14012,0.265048 0.276135,0.524729 0.375,0.8125 0.01041,0.03078 0.02133,0.06274 0.03125,0.09375 0.09046,0.278085 0.1388,0.579404 0.1875,0.875 C 89.95865,11.3493 90,11.665279 90,12 l 0,2 0,68 0,2 c 0,3.324 -2.676,6 -6,6 l -72,0 z" + inkscape:connector-curvature="0" + id="path3615" + style="opacity:0.2;fill:url(#radialGradient3619);fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <rect + width="66" + height="66" + rx="12" + ry="12" + x="15" + y="15" + clip-path="url(#clipPath3613)" + id="rect3171" + style="opacity:0.1;fill:url(#linearGradient3613);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter3794)" /> + </g> + <g + id="layer3"> + <path + d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,12 84,0 0,-12 C 90,8.676 87.324,6 84,6 L 12,6 z" + inkscape:connector-curvature="0" + id="rect4237" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <rect + width="84" + height="6" + x="6" + y="24" + id="rect3250" + style="opacity:0.8;fill:url(#linearGradient4262);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <rect + width="84" + height="5" + x="6" + y="15" + id="rect4289" + style="opacity:0.3;fill:url(#linearGradient4320);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <g + transform="translate(1,0)" + id="g4299"> + <path + d="M 28.333333,6 23,15 28.333333,24 39,24 33.666667,15 39,6 28.333333,6 z" + inkscape:connector-curvature="0" + id="path4293" + style="fill:#f0f0f0;fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <path + d="M 50.333333,6 45,15 50.333333,24 61,24 55.666667,15 61,6 50.333333,6 z" + inkscape:connector-curvature="0" + id="path4295" + style="fill:#f0f0f0;fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <path + d="M 72.333333,6 67,15 72.333333,24 83,24 77.666667,15 83,6 72.333333,6 z" + inkscape:connector-curvature="0" + id="path4297" + style="fill:#f0f0f0;fill-opacity:1;fill-rule:evenodd;stroke:none" /> + </g> + <rect + width="84" + height="5" + x="6" + y="10" + id="rect4291" + style="opacity:0.7;fill:url(#linearGradient4310);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <rect + width="84" + height="5" + x="6" + y="19" + id="rect4324" + style="opacity:0.7;fill:url(#linearGradient4326);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <path + d="m 12.005391,10.5 c 1.109513,0 1.770155,0.983693 6.03003,4.302704 4.304387,3.353689 5.477493,3.726472 5.477493,5.074645 0,1.348423 -1.088205,1.622651 -2.439921,1.622651 l -8.133072,0 C 11.588205,21.5 10.5,20.414446 10.5,19.066023 l 0,-6.490605 C 10.5,11.226995 10.967611,10.5 12.005391,10.5 z" + inkscape:connector-curvature="0" + id="path4333" + style="fill:url(#linearGradient4371);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient4373);stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /> + <path + d="m 12.75,14.9375 a 1.4375,1.4375 0 1 1 -2.875,0 1.4375,1.4375 0 1 1 2.875,0 z" + transform="matrix(0.81330712,0,0,0.81132563,3.5868895,1.0746776)" + id="path4335" + style="fill:url(#radialGradient4379);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <path + d="m 12.75,14.9375 a 1.4375,1.4375 0 1 1 -2.875,0 1.4375,1.4375 0 1 1 2.875,0 z" + transform="matrix(0.81330712,0,0,0.81132563,3.5868895,7.09897)" + id="path4337" + style="fill:url(#radialGradient4377);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + <path + d="m 12.75,14.9375 a 1.4375,1.4375 0 1 1 -2.875,0 1.4375,1.4375 0 1 1 2.875,0 z" + transform="matrix(0.81330712,0,0,0.81132563,11.618297,7.09897)" + id="path4339" + style="fill:url(#radialGradient4375);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + </g> + <g + id="layer4"> + <path + d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,2 0,68 0,2 c 0,0.334721 0.04135,0.6507 0.09375,0.96875 0.0487,0.295596 0.09704,0.596915 0.1875,0.875 0.00988,0.03038 0.020892,0.0636 0.03125,0.09375 0.098865,0.287771 0.2348802,0.547452 0.375,0.8125 0.1445918,0.273507 0.3156161,0.535615 0.5,0.78125 0.1843839,0.245635 0.3737765,0.473472 0.59375,0.6875 0.439947,0.428056 0.94291,0.814526 1.5,1.09375 0.278545,0.139612 0.5734731,0.246947 0.875,0.34375 -0.2562018,-0.100222 -0.4867109,-0.236272 -0.71875,-0.375 -0.00741,-0.0044 -0.023866,0.0045 -0.03125,0 -0.031933,-0.0193 -0.062293,-0.04251 -0.09375,-0.0625 -0.120395,-0.0767 -0.2310226,-0.163513 -0.34375,-0.25 -0.1061728,-0.0808 -0.2132809,-0.161112 -0.3125,-0.25 C 8.4783201,88.557317 8.3087904,88.373362 8.15625,88.1875 8.0486711,88.057245 7.9378561,87.922215 7.84375,87.78125 7.818661,87.74287 7.805304,87.69538 7.78125,87.65625 7.716487,87.553218 7.6510225,87.451733 7.59375,87.34375 7.4927417,87.149044 7.3880752,86.928049 7.3125,86.71875 7.30454,86.69694 7.288911,86.6782 7.28125,86.65625 7.2494249,86.5643 7.2454455,86.469419 7.21875,86.375 7.1884177,86.268382 7.1483606,86.171969 7.125,86.0625 7.0521214,85.720988 7,85.364295 7,85 L 7,83 7,15 7,13 C 7,10.218152 9.2181517,8 12,8 l 2,0 68,0 2,0 c 2.781848,0 5,2.218152 5,5 l 0,2 0,68 0,2 c 0,0.364295 -0.05212,0.720988 -0.125,1.0625 -0.04415,0.206893 -0.08838,0.397658 -0.15625,0.59375 -0.0077,0.02195 -0.0233,0.04069 -0.03125,0.0625 -0.06274,0.173739 -0.138383,0.367449 -0.21875,0.53125 -0.04158,0.0828 -0.07904,0.169954 -0.125,0.25 -0.0546,0.09721 -0.126774,0.18835 -0.1875,0.28125 -0.09411,0.140965 -0.204921,0.275995 -0.3125,0.40625 -0.143174,0.17445 -0.303141,0.346998 -0.46875,0.5 -0.01117,0.0102 -0.01998,0.02115 -0.03125,0.03125 -0.138386,0.125556 -0.285091,0.234436 -0.4375,0.34375 -0.102571,0.07315 -0.204318,0.153364 -0.3125,0.21875 -0.0074,0.0045 -0.02384,-0.0044 -0.03125,0 -0.232039,0.138728 -0.462548,0.274778 -0.71875,0.375 0.301527,-0.0968 0.596455,-0.204138 0.875,-0.34375 0.55709,-0.279224 1.060053,-0.665694 1.5,-1.09375 0.219973,-0.214028 0.409366,-0.441865 0.59375,-0.6875 0.184384,-0.245635 0.355408,-0.507743 0.5,-0.78125 0.14012,-0.265048 0.276135,-0.524729 0.375,-0.8125 0.01041,-0.03078 0.02133,-0.06274 0.03125,-0.09375 0.09046,-0.278085 0.1388,-0.579404 0.1875,-0.875 C 89.95865,84.6507 90,84.334721 90,84 l 0,-2 0,-68 0,-2 C 90,8.676 87.324,6 84,6 L 12,6 z" + inkscape:connector-curvature="0" + id="rect3728" + style="opacity:0.3;fill:url(#linearGradient3188);fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </g> + <g + id="g4513" + transform="matrix(0.6394564,0,0,0.6394564,84.664912,2.0456889)"> + <path + style="fill:url(#linearGradient3157-9);fill-opacity:1;stroke:#34870e;stroke-width:2.34574246;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path3261" + inkscape:connector-curvature="0" + d="m -65.015175,50.278074 0,54.903746 c -3.855793,-1.50998 -8.894464,-1.76801 -14.09417,-0.36139 -9.717741,2.62883 -16.233352,10.00877 -14.521267,16.45962 1.712085,6.45086 10.979991,9.56093 20.697733,6.9321 8.256133,-2.23343 14.170779,-7.8963 14.718388,-13.50281 l 0.06571,0 0,-40.377001 35.186145,4.369521 0,39.09572 c -3.855791,-1.50998 -8.894464,-1.76801 -14.09417,-0.36139 -9.717739,2.62883 -16.233352,10.00876 -14.521267,16.45962 1.712087,6.45086 10.979991,9.56093 20.697733,6.9321 8.256135,-2.23343 14.170779,-7.8963 14.718388,-13.50281 l 0.06571,0 0,-70.542025 -48.918927,-6.505001 z m 6.866391,13.212595 35.186145,4.960885 0,4.435229 -35.186145,-4.369522 0,-5.026592 z" /> + <path + inkscape:connector-curvature="0" + style="fill:url(#radialGradient3159-7);fill-opacity:1;stroke:none" + id="path2494" + transform="matrix(1.0233043,-0.24105639,0.24105639,1.0233043,-138.19892,40.809487)" + d="m 56.391766,82.480003 a 14.672467,9.2807773 0 1 1 -29.344933,0 14.672467,9.2807773 0 1 1 29.344933,0 z" /> + <path + inkscape:connector-curvature="0" + style="fill:url(#radialGradient3161-3);fill-opacity:1;stroke:none" + id="path3278" + transform="matrix(1.0233043,-0.24105639,0.24105639,1.0233043,-96.754882,53.447131)" + d="m 56.391766,82.480003 a 14.672467,9.2807773 0 1 1 -29.344933,0 14.672467,9.2807773 0 1 1 29.344933,0 z" /> + <path + style="fill:none;stroke:url(#radialGradient3163-8);stroke-width:3.12765646;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path3282" + inkscape:connector-curvature="0" + d="m -61.35495,88.196416 0,-33.510614 24.18021,3.942425" /> + <path + style="fill:none;stroke:url(#radialGradient3222);stroke-width:4.93222046;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path3292" + inkscape:connector-curvature="0" + d="m -19.212614,107.89627 0,-42.617148" /> + </g> +</svg> diff --git a/rapid/glade3/video24.png b/rapid/glade3/video24.png Binary files differdeleted file mode 100644 index df8b22b..0000000 --- a/rapid/glade3/video24.png +++ /dev/null diff --git a/rapid/glade3/video66.png b/rapid/glade3/video66.png Binary files differnew file mode 100644 index 0000000..5502609 --- /dev/null +++ b/rapid/glade3/video66.png diff --git a/rapid/glade3/video_small.png b/rapid/glade3/video_small.png Binary files differdeleted file mode 100644 index 74e7c0f..0000000 --- a/rapid/glade3/video_small.png +++ /dev/null diff --git a/rapid/glade3/video_small_shadow.png b/rapid/glade3/video_small_shadow.png Binary files differdeleted file mode 100644 index bf39c21..0000000 --- a/rapid/glade3/video_small_shadow.png +++ /dev/null diff --git a/rapid/gnomeglade.py b/rapid/gnomeglade.py deleted file mode 100644 index c0b0860..0000000 --- a/rapid/gnomeglade.py +++ /dev/null @@ -1,166 +0,0 @@ -### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org> - -### 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -"""Utility classes for working with glade files. - -""" -# modified by Damon Lynch May 2009 to update i18n - -import gtk -import gtk.glade -import gnome -import gnome.ui -#import gettext -import config -from common import Configi18n - -class Base(object): - """Base class for all glade objects. - - This class handles loading the xml glade file and connects - all methods name 'on_*' to the signals in the glade file. - - The handle to the xml file is stored in 'self.xml'. The - toplevel widget is stored in 'self.widget'. - - In addition it calls widget.set_data("pyobject", self) - this - allows us to get the python object given only the 'raw' gtk+ - object, which is sadly sometimes necessary. - """ - - def __init__(self, file, root, override={}): - """Load the widgets from the node 'root' in file 'file'. - - Automatically connects signal handlers named 'on_*'. - """ - global _ - _ = Configi18n._ - if Configi18n.locale_path: - gtk.glade.bindtextdomain(config.APP_NAME, Configi18n.locale_path) - gtk.glade.textdomain(config.APP_NAME) - self.xml = gtk.glade.XML(file, root, typedict=override ) - handlers = {} - for h in filter(lambda x:x.startswith("on_"), dir(self.__class__)): - handlers[h] = getattr(self, h) - self.xml.signal_autoconnect( handlers ) - self.widget = getattr(self, root) - self.widget.set_data("pyobject", self) - - def __getattr__(self, key): - """Allow glade widgets to be accessed as self.widgetname. - """ - widget = self.xml.get_widget(key) - if widget: # cache lookups - setattr(self, key, widget) - return widget - raise AttributeError(key) - - def flushevents(self): - """Handle all the events currently in the main queue and return. - """ - while gtk.events_pending(): - gtk.main_iteration(); - - def _map_widgets_into_lists(self, widgetnames): - """Put sequentially numbered widgets into lists. - - e.g. If an object had widgets self.button0, self.button1, ..., - then after a call to object._map_widgets_into_lists(["button"]) - object has an attribute self.button == [self.button0, self.button1, ...]." - """ - for item in widgetnames: - setattr(self,item, []) - lst = getattr(self,item) - i = 0 - while 1: - key = "%s%i"%(item,i) - try: - val = getattr(self, key) - except AttributeError: - break - lst.append(val) - i += 1 - - -class Component(Base): - """A convenience base class for widgets which use glade. - """ - - def __init__(self, file, root, override={}): - Base.__init__(self, file, root, override) - - -class GtkApp(Base): - """A convenience base class for gtk+ apps created in glade. - """ - - def __init__(self, file, root=None): - Base.__init__(self, file, root) - - def main(self): - """Enter the gtk main loop. - """ - gtk.main() - - def quit(self, *args): - """Signal the gtk main loop to quit. - """ - gtk.main_quit() - - -class GnomeApp(GtkApp): - """A convenience base class for apps created in glade. - """ - - def __init__(self, name, version, file, root): - """Initialise program 'name' and version from 'file' containing root node 'root'. - """ - self.program = gnome.program_init(name, version) - GtkApp.__init__(self,file,root) - if 0: - self.client = gnome.ui.Client() - self.client.disconnect() - def connected(*args): - print "CONNECTED", args - def cb(name): - def cb2(*args): - print name, args, "\n" - return cb2 - self.client.connect("connect", cb("CON")) - self.client.connect("die", cb("DIE")) - self.client.connect("disconnect", cb("DIS")) - self.client.connect("save-yourself", cb("SAVE")) - self.client.connect("shutdown-cancelled", cb("CAN")) - self.client.connect_to_session_manager() - - -def load_pixbuf(fname, size=0): - """Load an image from a file as a pixbuf, with optional resizing. - """ - image = gtk.Image() - image.set_from_file(fname) - image = image.get_pixbuf() - if size: - aspect = float(image.get_height()) / image.get_width() - image = image.scale_simple(size, int(aspect*size), 2) - return image - -def url_show(url): - return gnome.url_show(url) - -def FileEntry(*args): - return gnome.ui.FileEntry(*args) - diff --git a/rapid/idletube.py b/rapid/idletube.py deleted file mode 100644 index 0b07536..0000000 --- a/rapid/idletube.py +++ /dev/null @@ -1,205 +0,0 @@ - -# Copyright (c) 2005 Antoon Pardon -# -# Modified 2010 by Damon Lynch to use python's higher performance deque, rather than a regular list -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import collections -from Queue import Queue - -from threading import Lock -from thread import get_ident - -from types import BooleanType as UnConnected - -UnRegistered, Registered = False, True - - -class EOInformation(Exception): - pass - -class TubeAccess(Exception): - pass - - -class Fifo: - - def __init__(self): - self.fifo = collections.deque() - - def put(self, item): - self.fifo.append(item) - - def get(self): - return self.fifo.popleft() - - def size(self): - return len(self.fifo) - - -class Tube: - - def __init__(self, maxsize, lck = Lock, container = None): - if container is None: - container = Fifo() - self.readers = set() - self.writers = set() - self.container = container - self.maxsize = maxsize - self.cb_arglst = [] - self.cb_src = UnRegistered - self.in_use = Lock() - self.nowriter = lck() - self.full = lck() - self.empty = lck() - self.empty.acquire() - self.nowriter.acquire() - - def open(self, access = 'r', *to): - thrd = get_ident() - access = access.lower() - self.in_use.acquire() - if 'w' in access: - if len(self.writers) == 0: - for _ in self.readers: - self.nowriter.release() - self.writers.add(thrd) - if 'r' in access: - self.readers.add(thrd) - if len(self.writers) == 0: - self.in_use.release() - self.nowriter.acquire(*to) - else: - self.in_use.release() - else: - self.in_use.release() - - def close(self, access = 'rw'): - thrd = get_ident() - access = access.lower() - self.in_use.acquire() - if 'r' in access: - self.readers.discard(thrd) - if 'w' in access: - self.writers.discard(thrd) - if len(self.writers) == 0: - if self.container.size() == 0: - self.empty.release() - if self.cb_src is Registered and len(self.readers) > 0: - self.cb_src = gob.idle_add(self._idle_callback) - for _ in self.readers: - self.container.put(EOInformation) - self.in_use.release() - - def size(self): - self.in_use.acquire() - size = self.container.size() - self.in_use.release() - return size - - def get(self, *to): - thrd = get_ident() - if thrd not in self.readers: - raise TubeAccess, "Thread has no read access for tube" - self.empty.acquire(*to) - self.in_use.acquire() - size = self.container.size() - if size == self.maxsize: - self.full.release() - item = self.container.get() - if size != 1: - self.empty.release() - elif type(self.cb_src) is not UnConnected: - gob.source_remove(self.cb_src) - self.cb_src = Registered - self.in_use.release() - if item is EOInformation: - raise EOInformation - else: - return item - - def put(self, item, *to): - thrd = get_ident() - if thrd not in self.writers: - raise TubeAccess, "Thread has no write access for tube" - if thrd in self.readers: - self._put_rw(item) - else: - self._put_wo(item, *to) - - def _put_wo(self, item, *to): - self.full.acquire(*to) - self.in_use.acquire() - size = self.container.size() - if size == 0: - self.empty.release() - if self.cb_src is Registered: - self.cb_src = gob.idle_add(self._idle_callback) - self.container.put(item) - if size + 1 < self.maxsize: - self.full.release() - self.in_use.release() - - def _put_rw(self, item): - self.in_use.acquire() - size = self.container.size() - if size == 0: - self.empty.release() - if self.cb_src is Registered: - self.cb_src = gob.idle_add(self._idle_callback) - self.container.put(item) - self.in_use.release() - - def _idle_callback(self): - self.in_use.acquire() - lst = self.cb_arglst.pop(0) - self.in_use.release() - func = lst[0] - lst[0] = self - ret_val = func(*lst) - self.in_use.acquire() - if ret_val: - lst[0] = func - self.cb_arglst.append(lst) - elif self.cb_arglst == []: - self.cb_src = UnRegistered - self.in_use.release() - return self.cb_src is not UnRegistered - - -def tube_add_watch(tube, callback, *args): - - global gob - import gobject as gob - - tube.in_use.acquire() - tube.cb_arglst.append([callback] + list(args)) - if tube.cb_src is UnRegistered: - if tube.container.size() == 0: - tube.cb_src = Registered - else: - tube.cb_src = gob.idle_add(tube._idle_callback) - tube.in_use.release() - -def tube_remove_watch(tube): -## tube.in_use.acquire() -## gob.source_remove(tube.cb_src) -## tube._idle_callback.handler_block(tube.cb_src) - pass diff --git a/rapid/media.py b/rapid/media.py deleted file mode 100755 index 9819ab7..0000000 --- a/rapid/media.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import os -import sys -import types -import datetime -import subprocess - -import config -from config import max_thumbnail_size -from config import STATUS_NOT_DOWNLOADED, \ - STATUS_DOWNLOAD_PENDING, \ - STATUS_CANNOT_DOWNLOAD - - -import common -import metadata -import videometadata - -from common import Configi18n -global _ -_ = Configi18n._ - -import operator -import gtk - -def _getDefaultLocationLegacy(options, ignore_missing_dir=False): - if ignore_missing_dir: - return common.getFullPath(options[0]) - for default in options: - path = common.getFullPath(default) - if os.path.isdir(path): - return path - return common.getFullPath('') - -def _getDefaultLocationXDG(dir_type): - proc = subprocess.Popen(['xdg-user-dir', dir_type], stdout=subprocess.PIPE) - output = proc.communicate()[0].strip() - return output - -def getDefaultPhotoLocation(ignore_missing_dir=False): - try: - return _getDefaultLocationXDG('PICTURES') - except: - return _getDefaultLocationLegacy(config.DEFAULT_PHOTO_LOCATIONS, ignore_missing_dir) - -def getDefaultVideoLocation(ignore_missing_dir=False): - try: - return _getDefaultLocationXDG('VIDEOS') - except: - return _getDefaultLocationLegacy(config.DEFAULT_VIDEO_LOCATIONS, ignore_missing_dir) - -def getDefaultBackupPhotoIdentifier(): - return os.path.split(getDefaultPhotoLocation(ignore_missing_dir = True))[1] - -def getDefaultBackupVideoIdentifier(): - return os.path.split(getDefaultVideoLocation(ignore_missing_dir = True))[1] - -def is_DCIM_Media(path): - """ Returns true if directory specifies some media with photos on it """ - - if os.path.isdir(os.path.join(path, "DCIM")): - # is very likely a memory card, or something like that! - return True - else: - return False - - -def isBackupMedia(path, identifiers, writeable=True): - """ Test to see if path is used as a backup medium for storing photos or videos - - Identifiers is expected to be a list of folder names to check to see - if the path is a backup path. Only one of them needs to be present - for the path to be considered a backup medium. - - If writeable is True, the directory must be writeable by the user """ - suitable = False - - for identifier in identifiers: - if os.path.isdir(os.path.join(path, identifier)): - if writeable: - suitable = os.access(os.path.join(path, identifier), os.W_OK) - else: - suitable = True - if suitable: - return True - return False - -def isImage(fileName): - ext = os.path.splitext(fileName)[1].lower()[1:] - return (ext in metadata.RAW_FILE_EXTENSIONS) or (ext in metadata.NON_RAW_IMAGE_FILE_EXTENSIONS) - -def isVideo(fileName): - ext = os.path.splitext(fileName)[1].lower()[1:] - return (ext in videometadata.VIDEO_FILE_EXTENSIONS) - - -class MediaFile: - """ - A photo or video file, with metadata - """ - - def __init__(self, thread_id, name, path, size, fileSystemModificationTime, deviceName, downloadFolder, volume, isPhoto = True): - self.thread_id = thread_id - self.path = path - self.name = name - self.fullFileName = os.path.join(path, name) - self.size = size # type int - self.modificationTime = fileSystemModificationTime - self.deviceName = deviceName - self.downloadFolder = downloadFolder - self.volume = volume - - self.jobcode = '' - - # a reference into the SelectionTreeView's liststore - self.treerowref = None - - # generated values - self.downloadSubfolder = '' - self.downloadPath = '' - self.downloadName = '' - self.downloadFullFileName = '' - - self.isImage = isPhoto - self.isVideo = not self.isImage - if isPhoto: - self.displayName = _("photo") - self.displayNameCap = _("Photo") - else: - self.displayName = _("video") - self.displayNameCap = _("Video") - - - self.metadata = None - self.thumbnail = None - self.genericThumbnail = False - self.sampleName = '' - self.sampleSubfolder = '' - self.samplePath = '' - - # whether the sample genereated name, subfolder and path need to be refreshed in a preview - self.sampleStale = False - - self.status = STATUS_NOT_DOWNLOADED - self.problem = None # class Problem in problemnotifcation.py - - def loadMetadata(self): - """ - Attempt to load the metadata for the photo or video - - Raises errors if unable to be loaded - """ - if not self.metadata: - if self.isImage: - self.metadata = metadata.MetaData(self.fullFileName) - self.metadata.read() - else: - self.metadata = videometadata.VideoMetaData(self.fullFileName) - - - def dateTime(self, alternative_if_date_missing=None): - date = None - if self.metadata: - date = self.metadata.dateTime() - if not date: - if alternative_if_date_missing: - date = alternative_if_date_missing - else: - date = datetime.datetime.fromtimestamp(self.modificationTime) - return date - - - def generateThumbnail(self, tempWorkingDir): - """ - Attempts to generate or extract a thumnail and its orientation for the photo or video - """ - if self.metadata is None: - sys.stderr.write("metadata should not be empty!") - else: - if self.isImage: - try: - thumbnail = self.metadata.getThumbnailData(max_thumbnail_size) - if not isinstance(thumbnail, types.StringType): - self.thumbnail = None - else: - orientation = self.metadata.orientation(missing=None) - pbloader = gtk.gdk.PixbufLoader() - pbloader.write(thumbnail) - pbloader.close() - # Get the resulting pixbuf and build an image to be displayed - pixbuf = pbloader.get_pixbuf() - if orientation == 8: - pixbuf = pixbuf.rotate_simple(gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) - elif orientation == 6: - pixbuf = pixbuf.rotate_simple(gtk.gdk.PIXBUF_ROTATE_CLOCKWISE) - elif orientation == 3: - pixbuf = pixbuf.rotate_simple(gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN) - - self.thumbnail = pixbuf - except: - pass - else: - # get thumbnail of video - # it may need to be generated - self.thumbnail = self.metadata.getThumbnailData(max_thumbnail_size, tempWorkingDir) - if self.thumbnail: - # scale to size - self.thumbnail = common.scale2pixbuf(max_thumbnail_size, max_thumbnail_size, self.thumbnail) - - - -class Media: - """ Generic class for media holding images and videos """ - def __init__(self, path, volume = None): - """ - volume is a gnomevfs or gio volume: see class Volume in rapid.py - """ - - self.path = path - self.volume = volume - - - def prettyName(self, limit=config.MAX_LENGTH_DEVICE_NAME): - """ - Returns a name for the media, useful for display. - - If the media is from a gnomevfs volume, returns the gnome name. - - Else. returns the last part of the mount point after stripping out - underscores. - """ - - if self.volume: - return self.volume.get_name(limit) - else: - name = os.path.split(self.path)[1] - name = name.replace('_', ' ') - v = name - if limit: - if len(v) > limit: - v = v[:limit] + '...' - return v - - def getPath(self): - return self.path - - -class CardMedia(Media): - """Compact Flash cards, hard drives, etc.""" - def __init__(self, path, volume = None): - """ - volume is a gnomevfs or gio volume, see class Volume in rapid.py - """ - Media.__init__(self, path, volume) - - - def setMedia(self, imagesAndVideos, fileSizeSum, noFiles): - self.imagesAndVideos = imagesAndVideos # class MediaFile - self.fileSizeSum = fileSizeSum - self.noFiles = noFiles - - def numberOfImagesAndVideos(self): - return self.noFiles - - def sizeOfImagesAndVideos(self, humanReadable = True): - if humanReadable: - return common.formatSizeForUser(self.fileSizeSum) - else: - return self.fileSizeSum - - def sizeAndNumberDownloadPending(self): - """ - Returns how many files have their status set to download pending, and their size - """ - v = s = 0 - fileIndex = [] - for i in range(len(self.imagesAndVideos)): - mediaFile = self.imagesAndVideos[i][0] - if mediaFile.status == STATUS_DOWNLOAD_PENDING: - v += 1 - s += mediaFile.size - fileIndex.append(i) - return (v, s, fileIndex) - - def numberOfFilesNotCannotDownload(self): - """ - Returns how many files whose status is not cannot download - """ - v = 0 - for i in range(len(self.imagesAndVideos)): - mediaFile = self.imagesAndVideos[i][0] - if mediaFile.status <> STATUS_CANNOT_DOWNLOAD: - v += 1 - - return v - - def downloadPending(self): - """ - Returns true if there a mediaFile with status download pending on the device. - Inefficient. Not currently used. - """ - for i in range(len(self.imagesAndVideos)): - mediaFile = self.imagesAndVideos[i][0] - if mediaFile.status == config.STATUS_DOWNLOAD_PENDING: - return True - return False - - def _firstFile(self, isImage): - if self.imagesAndVideos: - for i in range(len(self.imagesAndVideos)): - if self.imagesAndVideos[i][0].isImage == isImage: - return self.imagesAndVideos[i][0] - else: - return None - - def firstImage(self): - return self._firstFile(True) - - def firstVideo(self): - return self._firstFile(False) - diff --git a/rapid/metadata.py b/rapid/metadataphoto.py index 1a040f3..8a760c4 100755 --- a/rapid/metadata.py +++ b/rapid/metadataphoto.py @@ -20,7 +20,7 @@ import re import datetime import sys -import subprocess +#~ import subprocess import config import types import time @@ -31,50 +31,7 @@ except ImportError: sys.stderr.write("You need to install pyexiv2, the python binding for exiv2, to run this program.\n" ) sys.exit(1) -#only pyexiv2 <= 0.1.1 does not use the "Rational" class -if 'Rational' in dir(pyexiv2): - usesRational = True -else: - usesRational = False -#get versions of pyexiv2 and exiv2 libraries -if 'version_info' in dir(pyexiv2): - pyexiv2_version = pyexiv2.version_info - exiv2_version = pyexiv2.exiv2_version_info - baseclass = eval('pyexiv2.metadata.ImageMetadata') -else: - pyexiv2_version = (0,1,'x') - # try to determine the version of exiv2 from it's standard output - try: - proc = subprocess.Popen(['exiv2', '-V'], stdout=subprocess.PIPE) - output = proc.communicate()[0] - except: - output = None - exiv2_version = None - if output: - # assume output contains the line 'exiv2 0.x' or possibly - # 'exiv2 0.x.x' - start = output.find('exiv2 ') - if start < 0: - exiv2_version = None - else: - end = output.find('\n', start) - if end: - exiv2_v = output[6:end] - else: - exiv2_v = output[6:] - - exiv2_version = [] - dot = exiv2_v.find('.') - while dot > 0: - exiv2_version += [int(exiv2_v[:dot])] - exiv2_v = exiv2_v[dot+1:] - dot = exiv2_v.find('.') - exiv2_version += [int(exiv2_v)] - exiv2_version = tuple(exiv2_version) - - - baseclass = eval('pyexiv2.Image') def __version_info(version): if not version: @@ -85,46 +42,19 @@ def __version_info(version): v += '.%s' % i return v[1:] -def version_info(): - return __version_info(pyexiv2_version) +def pyexiv2_version_info(): + return __version_info(pyexiv2.version_info) def exiv2_version_info(): - return __version_info(exiv2_version) - -RAW_FILE_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mrw', - 'nef', 'orf', 'pef', 'raf', 'raw', 'sr2', 'srw'] - -#exiv2 0.18.1 introduces support for Panasonic .RW2 files -#pyexiv2 in combination with exiv2 0.18 segfaults when trying to read an -#RW2 files, so we should not read those! exiv2 0.17 & pyexiv2 segfaults -#with MEF files. - -if exiv2_version is not None: - if exiv2_version[0] > 0: - RAW_FILE_EXTENSIONS += ['rw2', 'mef'] - else: - if exiv2_version[1] > 17: - RAW_FILE_EXTENSIONS += ['mef'] - if exiv2_version[1] > 18: - RAW_FILE_EXTENSIONS += ['rw2'] - else: - if len(exiv2_version) > 2: - if exiv2_version[2] >= 1: - RAW_FILE_EXTENSIONS += ['rw2'] - -RAW_FILE_EXTENSIONS.sort() - -NON_RAW_IMAGE_FILE_EXTENSIONS = ['jpg', 'jpe', 'jpeg', 'tif', 'tiff'] + return __version_info(pyexiv2.exiv2_version_info) -class MetaData(baseclass): +class MetaData(pyexiv2.metadata.ImageMetadata): """ Class providing human readable access to image metadata """ - __version01__ = pyexiv2_version[0] == 0 and pyexiv2_version[1] == 1 - def aperture(self, missing=''): """ Returns in string format the floating point value of the image's aperture. @@ -133,12 +63,10 @@ class MetaData(baseclass): """ try: - if usesRational: - a = self["Exif.Photo.FNumber"] - a0, a1 = str(a).split('/') - else: - a0, a1 = self["Exif.Photo.FNumber"] - a = float(a0) / float(a1) + + a = self["Exif.Photo.FNumber"].value + + a = float(a.numerator) / float(a.denominator) return "%.1f" % a except: return missing @@ -150,11 +78,11 @@ class MetaData(baseclass): Returns missing if the metadata value is not present. """ try: - return "%s" % (self["Exif.Photo.ISOSpeedRatings"]) + return self["Exif.Photo.ISOSpeedRatings"].human_value except: return missing - def exposureTime(self, alternativeFormat=False, missing=''): + def exposure_time(self, alternativeFormat=False, missing=''): """ Returns in string format the exposure time of the image. @@ -180,20 +108,12 @@ class MetaData(baseclass): """ try: - if usesRational: - e = str(self["Exif.Photo.ExposureTime"]) + e = self["Exif.Photo.ExposureTime"].value + + e0 = int(e.numerator) + e1 = int(e.denominator) - e0, e1 = e.split('/') - e0 = int(e0) - e1 = int(e1) - # some values, e.g. Nikon, are in the format "10/1600" - if (e0 > 1) and (e0 < e1): - e1 = e1 / e0 - e0 = 1 - else: - e0, e1 = self["Exif.Photo.ExposureTime"] - if e1 > e0: if alternativeFormat: if e0 == 1: @@ -213,101 +133,93 @@ class MetaData(baseclass): except: return missing - def focalLength(self, missing=''): + def focal_length(self, missing=''): """ Returns in string format the focal length of the lens used to record the image. Returns missing if the metadata value is not present. """ try: - if usesRational: - f = str(self["Exif.Photo.FocalLength"]) - f0, f1 = f.split('/') - else: - f0, f1 = self["Exif.Photo.FocalLength"] + f = self["Exif.Photo.FocalLength"].value + f0 = float(f.numerator) + f1 = float(f.denominator) - f0 = float(f0) - if not f1: - f1 = 1.0 - else: - f1 = float(f1) - return "%.0f" % (f0 / f1) except: return missing - def cameraMake(self, missing=''): + def camera_make(self, missing=''): """ Returns in string format the camera make (manufacturer) used to record the image. Returns missing if the metadata value is not present. """ try: - return self["Exif.Image.Make"].strip() + return self["Exif.Image.Make"].value.strip() except: return missing - def cameraModel(self, missing=''): + def camera_model(self, missing=''): """ Returns in string format the camera model used to record the image. Returns missing if the metadata value is not present. """ try: - return self["Exif.Image.Model"].strip() + return self["Exif.Image.Model"].value.strip() except: return missing - def cameraSerial(self, missing=''): + def camera_serial(self, missing=''): try: - keys = self.rpd_keys() + keys = self.exif_keys if 'Exif.Canon.SerialNumber' in keys: - v = self['Exif.Canon.SerialNumber'] + v = self['Exif.Canon.SerialNumber'].raw_value elif 'Exif.Nikon3.SerialNumber' in keys: - v = self['Exif.Nikon3.SerialNumber'] + v = self['Exif.Nikon3.SerialNumber'].raw_value elif 'Exif.OlympusEq.SerialNumber' in keys: - v = self['Exif.OlympusEq.SerialNumber'] + v = self['Exif.OlympusEq.SerialNumber'].raw_value elif 'Exif.Olympus.SerialNumber' in keys: - v = self['Exif.Olympus.SerialNumber'] + v = self['Exif.Olympus.SerialNumber'].raw_value elif 'Exif.Olympus.SerialNumber2' in keys: - v = self['Exif.Olympus.SerialNumber2'] + v = self['Exif.Olympus.SerialNumber2'].raw_value elif 'Exif.Panasonic.SerialNumber' in keys: - v = self['Exif.Panasonic.SerialNumber'] + v = self['Exif.Panasonic.SerialNumber'].raw_value elif 'Exif.Fujifilm.SerialNumber' in keys: - v = self['Exif.Fujifilm.SerialNumber'] + v = self['Exif.Fujifilm.SerialNumber'].raw_value elif 'Exif.Image.CameraSerialNumber' in keys: - v = self['Exif.Image.CameraSerialNumber'] + v = self['Exif.Image.CameraSerialNumber'].raw_value else: return missing - v = str(v) + v = str(v) # probably not necessary, but just in case return v.strip() except: return missing - def shutterCount(self, missing=''): + def shutter_count(self, missing=''): try: - keys = self.rpd_keys() + keys = self.exif_keys if 'Exif.Nikon3.ShutterCount' in keys: - v = self['Exif.Nikon3.ShutterCount'] + v = self['Exif.Nikon3.ShutterCount'].raw_value elif 'Exif.Canon.FileNumber' in keys: - v = self['Exif.Canon.FileNumber'] + v = self['Exif.Canon.FileNumber'].raw_value elif 'Exif.Canon.ImageNumber' in keys: - v = self['Exif.Canon.ImageNumber'] + v = self['Exif.Canon.ImageNumber'].raw_value else: return missing return str(v) except: return missing - def ownerName(self, missing=''): + def owner_name(self, missing=''): """ returns camera name recorded by select Canon cameras""" try: - return self['Exif.Canon.OwnerName'].strip() + return self['Exif.Canon.OwnerName'].value.strip() except: return missing - def shortCameraModel(self, includeCharacters = '', missing=''): + def short_camera_model(self, includeCharacters = '', missing=''): """ Returns in shorterned string format the camera model used to record the image. @@ -359,7 +271,7 @@ class MetaData(baseclass): Note: assume exif values are in ENGLISH, regardless of current platform """ - m = self.cameraModel() + m = self.camera_model() m = m.replace(' Mark ', 'Mk') if m: s = r"(?:[^a-zA-Z0-9%s]?)(?P<model>[a-zA-Z0-9%s]*\d+[a-zA-Z0-9%s]*)"\ @@ -372,25 +284,9 @@ class MetaData(baseclass): return model else: return missing - - def filterMangledDates(self, d): - """ - Some EXIF dates are badly formed. Try to fix them - """ - - _datetime = d.strip() - # remove any weird characters at the end of the string - while _datetime and not _datetime[-1].isdigit(): - _datetime = _datetime[:-1] - _date, _time = _datetime.split(' ') - _datetime = "%s %s" % (_date.replace(":", "-") , _time.replace("-", ":")) - try: - d = datetime.datetime.strptime(_datetime, '%Y-%m-%d %H:%M:%S') - except: - d = None - return d + - def dateTime(self, missing=''): + def date_time(self, missing=''): """ Returns in python datetime format the date and time the image was recorded. @@ -400,22 +296,18 @@ class MetaData(baseclass): Returns missing either metadata value is not present. """ - keys = self.rpd_keys() try: - if "Exif.Photo.DateTimeOriginal" in keys: - v = self["Exif.Photo.DateTimeOriginal"] + if "Exif.Photo.DateTimeOriginal" in self.exif_keys: + v = self["Exif.Photo.DateTimeOriginal"].value else: - v = self["Exif.Image.DateTime"] - if isinstance(v, types.StringType): - v = self.filterMangledDates(v) - if v is None: - v = missing + v = self["Exif.Image.DateTime"].value + return v except: return missing - def timeStamp(self, missing=''): - dt = self.dateTime(missing=None) + def time_stamp(self, missing=''): + dt = self.date_time(missing=None) if not dt is None: try: t = dt.timetuple() @@ -426,10 +318,10 @@ class MetaData(baseclass): ts = missing return ts - def subSeconds(self, missing='00'): + def sub_seconds(self, missing='00'): """ returns the subsecond the image was taken, as recorded by the camera""" try: - return str(self["Exif.Photo.SubSecTimeOriginal"]) + return str(self["Exif.Photo.SubSecTimeOriginal"].value) except: return missing @@ -439,66 +331,13 @@ class MetaData(baseclass): Return type int """ try: - v = self['Exif.Image.Orientation'] + v = self['Exif.Image.Orientation'].value if isinstance(v, types.StringType): - # pyexiv2 >= 0.2 returns a string, not an int v = int(v) return v except: return missing - # following class methods are designed to cope with using both - # pyexiv2 0.1.x and pyexiv2 0.2.x - - def getThumbnailData(self, max_size_needed=0): - """ - Returns a thumbnail of the image. - - If the image supports multiple thumbnails, and max_size_needed - is not 0, then it will search for the smallest thumbnail that - matches the size required - - The image will be in whatever format the thumbnail itself is, - typically a jpeg or tiff. - """ - if self.__version01__: - return pyexiv2.Image.getThumbnailData(self)[1] - - else: - if not self.previews: - return None, None - else: - if max_size_needed: - for thumbnail in self.previews: - if thumbnail.dimensions[0] >= max_size_needed or thumbnail.dimensions[1] >= max_size_needed: - break - else: - thumbnail = self.previews[-1] - - return thumbnail.data - - def read(self): - if self.__version01__: - self.readMetadata() - else: - pyexiv2.metadata.ImageMetadata.read(self) - - def rpd_keys(self): - if self.__version01__: - return pyexiv2.Image.exifKeys(self) - else: - return self.exif_keys - - def __getitem__(self, key): - if self.__version01__: - v = pyexiv2.Image.__getitem__(self, key) - else: - v = pyexiv2.metadata.ImageMetadata.__getitem__(self, key).raw_value - # strip out null bytes from strings - if isinstance(v, types.StringType): - v = v.replace('\x00', '') - return v - class DummyMetaData(MetaData): """ @@ -522,34 +361,34 @@ class DummyMetaData(MetaData): def iso(self, missing=''): return "100" - def exposureTime(self, alternativeFormat=False, missing=''): + def exposure_time(self, alternativeFormat=False, missing=''): if alternativeFormat: return "4000" else: return "1/4000" - def focalLength(self, missing=''): + def focal_length(self, missing=''): return "135" - def cameraMake(self, missing=''): + def camera_make(self, missing=''): return "Canon" - def cameraModel(self, missing=''): + def camera_model(self, missing=''): return "Canon EOS 5D" - def shortCameraModel(self, includeCharacters = '', missing=''): + def short_camera_model(self, includeCharacters = '', missing=''): return "5D" - def cameraSerial(self, missing=''): + def camera_serial(self, missing=''): return '730402168' - def shutterCount(self, missing=''): + def shutter_count(self, missing=''): return '387' - def ownerName(self, missing=''): + def owner_name(self, missing=''): return 'Photographer Name' - def dateTime(self, missing=''): + def date_time(self, missing=''): return datetime.datetime.now() def subSeconds(self, missing='00'): @@ -572,16 +411,16 @@ if __name__ == '__main__': print "f"+ m.aperture('missing ') print "ISO " + m.iso('missing ') - print m.exposureTime(missing='missing ') + " sec" - print m.exposureTime(alternativeFormat=True, missing='missing ') - print m.focalLength('missing ') + "mm" - print m.cameraMake() - print m.cameraModel() - print m.shortCameraModel() - print m.shortCameraModel(includeCharacters = "\-") - print m.dateTime() + print m.exposure_time(missing='missing ') + " sec" + print m.exposure_time(alternativeFormat=True, missing='missing ') + print m.focal_length('missing ') + "mm" + print m.camera_make() + print m.camera_model() + print m.short_camera_model() + print m.short_camera_model(includeCharacters = "\-") + print m.date_time() print m.orientation() - print 'Serial number:', m.cameraSerial() - print 'Shutter count:', m.shutterCount() - print 'Subseconds:', m.subSeconds() + print 'Serial number:', m.camera_serial() + print 'Shutter count:', m.shutter_count() + print 'Subseconds:', m.sub_seconds() diff --git a/rapid/videometadata.py b/rapid/metadatavideo.py index 77f6791..7b6bc6c 100755..100644 --- a/rapid/videometadata.py +++ b/rapid/metadatavideo.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: latin1 -*- -### Copyright (C) 2007-10 Damon Lynch <damonlynch@gmail.com> +### Copyright (C) 2007-11 Damon Lynch <damonlynch@gmail.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 @@ -25,39 +25,29 @@ import time import subprocess import tempfile +import multiprocessing +import logging +logger = multiprocessing.get_logger() + import gtk -import media import paths -from filmstrip import add_filmstrip + +import rpdfile try: - import kaa.metadata from hachoir_core.cmd_line import unicodeFilename from hachoir_parser import createParser from hachoir_metadata import extractMetadata except ImportError: DOWNLOAD_VIDEO = False -VIDEO_THUMBNAIL_FILE_EXTENSIONS = ['thm'] -VIDEO_FILE_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', 'tod'] - - - if DOWNLOAD_VIDEO: - - - try: - subprocess.check_call(["ffmpegthumbnailer", "-h"], stdout=subprocess.PIPE) - ffmpeg = True - except: - ffmpeg = False - def version_info(): from hachoir_metadata.version import VERSION return VERSION - def get_video_THM_file(fullFileName): + def get_video_THM_file(full_filename): """ Checks to see if a thumbnail file (THM) is in the same directory as the file. Expects a full path to be part of the file name. @@ -66,8 +56,8 @@ if DOWNLOAD_VIDEO: """ f = None - name, ext = os.path.splitext(fullFileName) - for e in VIDEO_THUMBNAIL_FILE_EXTENSIONS: + name, ext = os.path.splitext(full_filename) + for e in rpdfile.VIDEO_THUMBNAIL_FILE_EXTENSIONS: if os.path.exists(name + '.' + e): f = name + '.' + e break @@ -76,7 +66,7 @@ if DOWNLOAD_VIDEO: break return f - + class VideoMetaData(): def __init__(self, filename): """ @@ -85,13 +75,19 @@ if DOWNLOAD_VIDEO: self.filename = filename self.u_filename = unicodeFilename(filename) - self.parser = createParser(self.u_filename, self.filename) - self.metadata = extractMetadata(self.parser) - + self.metadata = None - def _kaa_get(self, key, missing, stream=None): + def _kaa_get(self, key, missing, stream=None): if not hasattr(self, 'info'): - self.info = kaa.metadata.parse(self.filename) + try: + from kaa.metadata import parse + except ImportError: + msg = """The package Kaa metadata does not exist. +It is needed to access FPS and codec video file metadata.""" + logger.error(msg) + self.info = None + else: + self.info = parse(self.filename) if self.info: if stream != None: v = self.info['video'][stream][key] @@ -104,23 +100,30 @@ if DOWNLOAD_VIDEO: else: return missing + def _load_hachoir_metadata_parser(self): + self.parser = createParser(self.u_filename, self.filename) + self.metadata = extractMetadata(self.parser) + def _get(self, key, missing): + if self.metadata is None: + self._load_hachoir_metadata_parser() + try: v = self.metadata.get(key) except: v = missing return v - def dateTime(self, missing=''): + def date_time(self, missing=''): return self._get('creation_date', missing) - def timeStamp(self, missing=''): + def time_stamp(self, missing=''): """ Returns a float value representing the time stamp, if it exists """ - dt = self.dateTime(missing=None) + dt = self.date_time(missing=None) if dt: - # convert it to a timestamp (not optimal, but better than nothing!) + # convert it to a time stamp (not optimal, but better than nothing!) v = time.mktime(dt.timetuple()) else: v = missing @@ -152,7 +155,7 @@ if DOWNLOAD_VIDEO: else: return None - def framesPerSecond(self, stream=0, missing=''): + def frames_per_second(self, stream=0, missing=''): fps = self._kaa_get('fps', missing, stream) try: fps = '%.0f' % float(fps) @@ -163,38 +166,8 @@ if DOWNLOAD_VIDEO: def fourcc(self, stream=0, missing=''): return self._kaa_get('fourcc', missing, stream) - def getThumbnailData(self, size, tempWorkingDir): - """ - Returns a pixbuf of the video's thumbnail + - If it cannot be created, returns None - """ - thm = get_video_THM_file(self.filename) - if thm: - thumbnail = gtk.gdk.pixbuf_new_from_file(thm) - aspect = float(thumbnail.get_height()) / thumbnail.get_width() - thumbnail = thumbnail.scale_simple(size, int(aspect*size), gtk.gdk.INTERP_BILINEAR) - thumbnail = add_filmstrip(thumbnail) - else: - if ffmpeg: - try: - tmp = tempfile.NamedTemporaryFile(dir=tempWorkingDir, prefix="rpd-tmp") - tmp.close() - except: - return None - - thm = os.path.join(tempWorkingDir, tmp.name) - - try: - subprocess.check_call(['ffmpegthumbnailer', '-i', self.filename, '-t', '10', '-f', '-o', thm, '-s', str(size)]) - thumbnail = gtk.gdk.pixbuf_new_from_file_at_size(thm, size, size) - os.unlink(thm) - except: - thumbnail = None - else: - thumbnail = None - return thumbnail - class DummyMetaData(): """ Class which gives metadata values for an imaginary video. @@ -204,10 +177,10 @@ class DummyMetaData(): See VideoMetaData class for documentation of class methods. """ - def __init__(self): + def __init__(self, filename): pass - def dateTime(self, missing=''): + def date_time(self, missing=''): return datetime.datetime.now() def codec(self, stream=0, missing=''): @@ -222,7 +195,7 @@ class DummyMetaData(): def height(self, stream=0, missing=''): return '1080' - def framesPerSecond(self, stream=0, missing=''): + def frames_per_second(self, stream=0, missing=''): return '24' def fourcc(self, stream=0, missing=''): @@ -239,12 +212,12 @@ if __name__ == '__main__': else: m = VideoMetaData(sys.argv[1]) - dt = m.dateTime() + dt = m.date_time() if dt: print dt.strftime('%Y%m%d-%H:%M:%S') print "codec: %s" % m.codec() print "%s seconds" % m.length() print "%sx%s" % (m.width(), m.height()) - print "%s fps" % m.framesPerSecond() + print "%s fps" % m.frames_per_second() print "Fourcc: %s" % (m.fourcc()) diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py new file mode 100644 index 0000000..0f46406 --- /dev/null +++ b/rapid/preferencesdialog.py @@ -0,0 +1,1656 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007 - 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import datetime + +import gtk + +import datetime +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import ValidatedEntry +import misc + +import config +import paths +import rpdfile +import higdefaults as hd +import metadataphoto +import metadatavideo +import tableplusminus as tpm + +import utilities + +import generatename as gn +from generatenameconfig import * +import problemnotification as pn + +from prefsrapid import format_pref_list_for_pretty_print, DownloadsTodayTracker + +from gettext import gettext as _ + +class PrefError(Exception): + """ base class """ + def unpackList(self, l): + """ + Make the preferences presentable to the user + """ + + s = '' + for i in l: + if i <> ORDER_KEY: + s += "'" + i + "', " + return s[:-2] + + def __str__(self): + return self.msg + +class PrefKeyError(PrefError): + def __init__(self, error): + value = error[0] + expectedValues = self.unpackList(error[1]) + self.msg = "Preference key '%(key)s' is invalid.\nExpected one of %(value)s" % { + 'key': value, 'value': expectedValues} + + +class PrefValueInvalidError(PrefKeyError): + def __init__(self, error): + value = error[0] + self.msg = "Preference value '%(value)s' is invalid" % {'value': value} + +class PrefLengthError(PrefError): + def __init__(self, error): + self.msg = "These preferences are not well formed:" + "\n %s" % self.unpackList(error) + +class PrefValueKeyComboError(PrefError): + def __init__(self, error): + self.msg = error + + +def check_pref_valid(pref_defn, prefs, modulo=3): + """ + Checks to see if prefs are valid according to definition. + + prefs is a list of preferences. + pref_defn is a Dict specifying what is valid. + modulo is how many list elements are equivalent to one line of preferences. + + Returns True if prefs match with pref_defn, + else raises appropriate error. + """ + + if (len(prefs) % modulo <> 0) or not prefs: + raise PrefLengthError(prefs) + else: + for i in range(0, len(prefs), modulo): + _check_pref_valid(pref_defn, prefs[i:i+modulo]) + + return True + +def _check_pref_valid(pref_defn, prefs): + + key = prefs[0] + value = prefs[1] + + + if pref_defn.has_key(key): + + next_pref_defn = pref_defn[key] + + if value == None: + # value should never be None, at any time + raise PrefValueInvalidError((None, next_pref_defn)) + + if next_pref_defn and not value: + raise gn.PrefValueInvalidError((value, next_pref_defn)) + + if type(next_pref_defn) == type({}): + return _check_pref_valid(next_pref_defn, prefs[1:]) + else: + if type(next_pref_defn) == type([]): + result = value in next_pref_defn + if not result: + raise gn.PrefValueInvalidError((value, next_pref_defn)) + return True + elif not next_pref_defn: + return True + else: + result = next_pref_defn == value + if not result: + raise gn.PrefKeyValue((value, next_pref_defn)) + return True + else: + raise PrefKeyError((key, pref_defn[ORDER_KEY])) + + +def filter_subfolder_prefs(pref_list): + """ + Filters out extraneous preference choices + """ + prefs_changed = False + continue_check = True + while continue_check and pref_list: + continue_check = False + if pref_list[0] == SEPARATOR: + # subfolder preferences should not start with a / + pref_list = pref_list[3:] + prefs_changed = True + continue_check = True + elif pref_list[-3] == SEPARATOR: + # subfolder preferences should not end with a / + pref_list = pref_list[:-3] + continue_check = True + prefs_changed = True + else: + for i in range(0, len(pref_list) - 3, 3): + if pref_list[i] == SEPARATOR and pref_list[i+3] == SEPARATOR: + # subfolder preferences should not contain two /s side by side + continue_check = True + prefs_changed = True + # note we are messing with the contents of the pref list, + # must exit loop and try again + pref_list = pref_list[:i] + pref_list[i+3:] + break + + return (prefs_changed, pref_list) + +class Comboi18n(gtk.ComboBox): + """ very simple i18n version of the venerable combo box + with one column displayed to the user. + + This combo box has two columns: + 1. the first contains the actual value and is invisible + 2. the second contains the translation of the first column, and this is what + the users sees + """ + def __init__(self): + liststore = gtk.ListStore(str, str) + gtk.ComboBox.__init__(self, liststore) + cell = gtk.CellRendererText() + self.pack_start(cell, True) + self.add_attribute(cell, 'text', 1) + # must name the combo box on pygtk used in Ubuntu 11.04, Fedora 15, etc. + self.set_name('GtkComboBox') + + def append_text(self, text): + model = self.get_model() + model.append((text, _(text))) + + def get_active_text(self): + model = self.get_model() + active = self.get_active() + if active < 0: + return None + return model[active][0] + +class PreferenceWidgets: + + def __init__(self, default_row, default_prefs, pref_defn_L0, pref_list): + self.default_row = default_row + self.default_prefs = default_prefs + self.pref_defn_L0 = pref_defn_L0 + self.pref_list = pref_list + + def _create_combo(self, choices): + combobox = Comboi18n() + for text in choices: + combobox.append_text(text) + return combobox + + def get_default_row(self): + """ + returns a list of default widgets + """ + return self.get_widgets_based_on_user_selection(self.default_row) + + def _get_pref_widgets(self, pref_definition, prefs, widgets): + key = prefs[0] + value = prefs[1] + + # supply a default value if the user has not yet chosen a value! + if not key: + key = pref_definition[ORDER_KEY][0] + + if not key in pref_definition: + raise gn.PrefKeyError((key, pref_definition.keys())) + + + list0 = pref_definition[ORDER_KEY] + + # the first widget will always be a combo box + widget0 = self._create_combo(list0) + widget0.set_active(list0.index(key)) + + widgets.append(widget0) + + if key == TEXT: + widget1 = gtk.Entry() + widget1.set_text(value) + + widgets.append(widget1) + widgets.append(None) + return + elif key in [SEPARATOR, JOB_CODE]: + widgets.append(None) + widgets.append(None) + return + else: + next_pref_definition = pref_definition[key] + if type(next_pref_definition) == type({}): + return self._get_pref_widgets(next_pref_definition, + prefs[1:], + widgets) + else: + if type(next_pref_definition) == type([]): + widget1 = self._create_combo(next_pref_definition) + if not value: + value = next_pref_definition[0] + try: + widget1.set_active(next_pref_definition.index(value)) + except: + raise gn.PrefValueInvalidError((value, next_pref_definition)) + + widgets.append(widget1) + else: + widgets.append(None) + + def _get_values_from_list(self): + for i in range(0, len(self.pref_list), 3): + yield (self.pref_list[i], self.pref_list[i+1], self.pref_list[i+2]) + + def get_widgets_based_on_prefs(self): + """ + Yields a list of widgets and their callbacks based on the users preferences. + + This list is equivalent to one row of preferences when presented to the + user in the Plus Minus Table. + """ + + for L0, L1, L2 in self._get_values_from_list(): + prefs = [L0, L1, L2] + widgets = [] + self._get_pref_widgets(self.pref_defn_L0, prefs, widgets) + yield widgets + + + def get_widgets_based_on_user_selection(self, selection): + """ + Returns a list of widgets and their callbacks based on what the user has selected. + + Selection is the values the user has chosen thus far in comboboxes. + It determines the contents of the widgets returned. + It should be a list of three values, with None for values not chosen. + For values which are None, the first value in the preferences + definition is chosen. + + """ + widgets = [] + + self._get_pref_widgets(self.pref_defn_L0, selection, widgets) + return widgets + + def check_prefs_for_validity(self): + """ + Checks preferences validity + """ + + return check_pref_valid(self.pref_defn_L0, self.pref_list) + +class PhotoNamePrefs(PreferenceWidgets): + def __init__(self, pref_list): + PreferenceWidgets.__init__(self, + default_row = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + default_prefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + pref_defn_L0 = DICT_IMAGE_RENAME_L0, + pref_list = pref_list) + +class VideoNamePrefs(PreferenceWidgets): + def __init__(self, pref_list): + PreferenceWidgets.__init__(self, + default_row = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + default_prefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + pref_defn_L0 = DICT_VIDEO_RENAME_L0, + pref_list = pref_list) + + +class PhotoSubfolderPrefs(PreferenceWidgets): + def __init__(self, pref_list): + + PreferenceWidgets.__init__(self, + default_row = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]], + default_prefs = DEFAULT_SUBFOLDER_PREFS, + pref_defn_L0 = DICT_SUBFOLDER_L0, + pref_list = pref_list) + + def filter_preferences(self): + filtered, pref_list = filter_subfolder_prefs(self.pref_list) + if filtered: + self.pref_list = pref_list + + def check_prefs_for_validity(self): + """ + Checks subfolder preferences validity above and beyond image name checks. + + See parent method for full description. + + Subfolders have additional requirments to that of file names. + """ + v = PreferenceWidgets.check_prefs_for_validity(self) + if v: + # peform additional checks: + # 1. do not start with a separator + # 2. do not end with a separator + # 3. do not have two separators in a row + # these three rules will ensure something else other than a + # separator is specified + L1s = [] + for i in range(0, len(self.pref_list), 3): + L1s.append(self.pref_list[i]) + + if L1s[0] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not start with a %s") % os.sep) + elif L1s[-1] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not end with a %s") % os.sep) + else: + for i in range(len(L1s) - 1): + if L1s[i] == SEPARATOR and L1s[i+1] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not contain two %s one after the other") % os.sep) + return v + +class VideoSubfolderPrefs(PhotoSubfolderPrefs): + def __init__(self, pref_list): + PreferenceWidgets.__init__(self, + default_row = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]], + default_prefs = DEFAULT_VIDEO_SUBFOLDER_PREFS, + pref_defn_L0 = DICT_VIDEO_SUBFOLDER_L0, + pref_list = pref_list) + +class RemoveAllJobCodeDialog(gtk.Dialog): + def __init__(self, parent_window, post_choice_callback): + gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, + gtk.STOCK_YES, gtk.RESPONSE_OK)) + + self.post_choice_callback = post_choice_callback + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + + prompt_hbox = gtk.HBox() + + icontheme = gtk.icon_theme_get_default() + icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) + if icon: + image = gtk.Image() + image.set_from_pixbuf(icon) + prompt_hbox.pack_start(image, False, False, padding = 6) + + prompt_label = gtk.Label(_('Should all Job Codes be removed?')) + prompt_label.set_line_wrap(True) + prompt_hbox.pack_start(prompt_label, False, False, padding=6) + + self.vbox.pack_start(prompt_hbox, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + self.set_default_response(gtk.RESPONSE_OK) + + + self.set_transient_for(parent_window) + self.show_all() + + + self.connect('response', self.on_response) + + def on_response(self, device_dialog, response): + user_selected = response == gtk.RESPONSE_OK + self.post_choice_callback(self, user_selected) + +class PhotoRenameTable(tpm.TablePlusMinus): + + def __init__(self, preferencesdialog, adjust_scroll_window): + + tpm.TablePlusMinus.__init__(self, 1, 3) + self.preferencesdialog = preferencesdialog + self.adjust_scroll_window = adjust_scroll_window + if not hasattr(self, "error_title"): + self.error_title = _("Error in Photo Rename preferences") + + self.table_type = self.error_title[len("Error in "):] + self.i = 0 + + if adjust_scroll_window: + self.scroll_bar = self.adjust_scroll_window.get_vscrollbar() + #this next line does not work on early versions of pygtk :( + self.scroll_bar.connect('visibility-notify-event', self.scrollbar_visibility_change) + self.connect("size-request", self.size_adjustment) + self.connect("add", self.size_adjustment) + self.connect("remove", self.size_adjustment) + + # get scrollbar thickness from parent app scrollbar - very hackish, but what to do?? + self.bump = 16# self.preferencesdialog.parentApp.image_scrolledwindow.get_hscrollbar().allocation.height + self.have_vertical_scrollbar = False + + + self.get_preferencesdialog_prefs() + self.setup_prefs_factory() + + try: + self.prefs_factory.check_prefs_for_validity() + + except (PrefValueInvalidError, PrefLengthError, + PrefValueKeyComboError, PrefKeyError), e: + + logger.error(self.error_title) + logger.error("Sorry, these preferences contain an error:") + logger.error(format_pref_list_for_pretty_print(self.prefs_factory.pref_list)) + + # the preferences were invalid + # reset them to their default + + self.pref_list = self.prefs_factory.default_prefs + self.setup_prefs_factory() + self.update_parentapp_prefs() + + msg = "%s.\n" % e + msg += "Resetting to default values." + logger.error(msg) + + + misc.run_dialog(self.error_title, msg, + preferencesdialog, + gtk.MESSAGE_ERROR) + + for row in self.prefs_factory.get_widgets_based_on_prefs(): + self.append(row) + + def update_preferences(self): + pref_list = [] + for row in self.pm_rows: + for col in range(self.pm_no_columns): + widget = row[col] + if widget: + name = widget.get_name() + if name == 'GtkComboBox': + value = widget.get_active_text() + elif name == 'GtkEntry': + value = widget.get_text() + else: + logger.critical("Program error: Unknown preference widget!") + value = '' + else: + value = '' + pref_list.append(value) + + self.pref_list = pref_list + self.update_parentapp_prefs() + self.prefs_factory.pref_list = pref_list + self.update_example() + + + def scrollbar_visibility_change(self, widget, event): + if event.state == gtk.gdk.VISIBILITY_UNOBSCURED: + self.have_vertical_scrollbar = True + self.adjust_scroll_window.set_size_request(self.adjust_scroll_window.allocation.width + self.bump, -1) + + + def size_adjustment(self, widget, arg2): + """ + Adjust scrolledwindow width in preferences dialog to reflect width of image rename table + + The algorithm is complicated by the need to take into account the presence of a vertical scrollbar, + which might be added as the user adds more rows + + The pygtk code behaves inconsistently depending on the pygtk version + """ + + if self.adjust_scroll_window: + self.have_vertical_scrollbar = self.scroll_bar.allocation.width > 1 or self.have_vertical_scrollbar + if not self.have_vertical_scrollbar: + if self.allocation.width > self.adjust_scroll_window.allocation.width: + self.adjust_scroll_window.set_size_request(self.allocation.width, -1) + else: + if self.allocation.width > self.adjust_scroll_window.allocation.width - self.bump: + self.adjust_scroll_window.set_size_request(self.allocation.width + self.bump, -1) + self.bump = 0 + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.image_rename + + + def setup_prefs_factory(self): + self.prefs_factory = PhotoNamePrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.image_rename = self.pref_list + + def update_example_job_code(self): + job_code = self.preferencesdialog.prefs.get_sample_job_code() + if not job_code: + job_code = _('Job code') + #~ self.prefs_factory.setJobCode(job_code) + + def update_example(self): + self.preferencesdialog.update_photo_rename_example() + + def get_default_row(self): + return self.prefs_factory.get_default_row() + + def on_combobox_changed(self, widget, row_position): + + for col in range(self.pm_no_columns): + if self.pm_rows[row_position][col] == widget: + break + selection = [] + for i in range(col + 1): + # ensure it is a combo box we are getting the value from + w = self.pm_rows[row_position][i] + name = w.get_name() + if name == 'GtkComboBox': + selection.append(w.get_active_text()) + else: + selection.append(w.get_text()) + + for i in range(col + 1, self.pm_no_columns): + selection.append('') + + if col <> (self.pm_no_columns - 1): + widgets = self.prefs_factory.get_widgets_based_on_user_selection(selection) + + for i in range(col + 1, self.pm_no_columns): + old_widget = self.pm_rows[row_position][i] + if old_widget: + self.remove(old_widget) + if old_widget in self.pm_callbacks: + del self.pm_callbacks[old_widget] + new_widget = widgets[i] + self.pm_rows[row_position][i] = new_widget + if new_widget: + self._create_callback(new_widget, row_position) + self.attach(new_widget, i, i+1, row_position, row_position + 1) + new_widget.show() + self.update_preferences() + + + def on_entry_changed(self, widget, row_position): + self.update_preferences() + + def on_row_added(self, row_position): + """ + Update preferences, as a row has been added + """ + self.update_preferences() + + # if this was the last row or 2nd to last row, and another has just been added, move vertical scrollbar down + if row_position in range(self.pm_no_rows - 3, self.pm_no_rows - 2): + adjustment = self.preferencesdialog.rename_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.upper) + + + def on_row_deleted(self, row_position): + """ + Update preferences, as a row has been deleted + """ + self.update_preferences() + +class VideoRenameTable(PhotoRenameTable): + def __init__(self, preferencesdialog, adjust_scroll_window): + self.error_title = _("Error in Video Rename preferences") + PhotoRenameTable.__init__(self, preferencesdialog, adjust_scroll_window) + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.video_rename + + def setup_prefs_factory(self): + self.prefs_factory = VideoNamePrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.video_rename = self.pref_list + + def update_example(self): + self.preferencesdialog.update_video_rename_example() + +class SubfolderTable(PhotoRenameTable): + """ + Table to display photo download subfolder preferences as part of preferences + dialog window. + """ + def __init__(self, preferencesdialog, adjust_scroll_window): + self.error_title = _("Error in Photo Download Subfolders preferences") + PhotoRenameTable.__init__(self, preferencesdialog, adjust_scroll_window) + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.subfolder + + def setup_prefs_factory(self): + self.prefs_factory = PhotoSubfolderPrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.subfolder = self.pref_list + + def update_example(self): + self.preferencesdialog.update_photo_download_folder_example() + +class VideoSubfolderTable(PhotoRenameTable): + def __init__(self, preferencesdialog, adjust_scroll_window): + self.error_title = _("Error in Video Download Subfolders preferences") + PhotoRenameTable.__init__(self, preferencesdialog, adjust_scroll_window) + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.video_subfolder + + def setup_prefs_factory(self): + self.prefs_factory = VideoSubfolderPrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.video_subfolder = self.pref_list + + def update_example(self): + self.preferencesdialog.update_video_download_folder_example() + +class RemoveAllJobCodeDialog(gtk.Dialog): + def __init__(self, parent_window, post_choice_callback): + gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, + gtk.STOCK_YES, gtk.RESPONSE_OK)) + + self.post_choice_callback = post_choice_callback + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + + prompt_hbox = gtk.HBox() + + icontheme = gtk.icon_theme_get_default() + icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) + if icon: + image = gtk.Image() + image.set_from_pixbuf(icon) + prompt_hbox.pack_start(image, False, False, padding = 6) + + prompt_label = gtk.Label(_('Should all Job Codes be removed?')) + prompt_label.set_line_wrap(True) + prompt_hbox.pack_start(prompt_label, False, False, padding=6) + + self.vbox.pack_start(prompt_hbox, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + self.set_default_response(gtk.RESPONSE_OK) + + self.set_transient_for(parent_window) + self.show_all() + + self.connect('response', self.on_response) + + def on_response(self, device_dialog, response): + user_selected = response == gtk.RESPONSE_OK + self.post_choice_callback(self, user_selected) + +class JobCodeDialog(gtk.Dialog): + """ Dialog prompting for a job code""" + + def __init__(self, parent_window, job_codes, default_job_code, post_job_code_entry_callback, entry_only): + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + gtk.Dialog.__init__(self, _('Enter a Job Code'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) + + + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + self.post_job_code_entry_callback = post_job_code_entry_callback + + self.combobox = gtk.combo_box_entry_new_text() + for text in job_codes: + self.combobox.append_text(text) + + self.job_code_hbox = gtk.HBox(homogeneous = False) + + if len(job_codes) and not entry_only: + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + task_label = gtk.Label(_('Enter a new Job Code, or select a previous one')) + else: + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + task_label = gtk.Label(_('Enter a new Job Code')) + task_label.set_line_wrap(True) + task_hbox = gtk.HBox() + task_hbox.pack_start(task_label, False, False, padding=6) + + label = gtk.Label(_('Job Code:')) + self.job_code_hbox.pack_start(label, False, False, padding=6) + self.job_code_hbox.pack_start(self.combobox, True, True, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + # make entry box have entry completion + self.entry = self.combobox.child + + completion = gtk.EntryCompletion() + completion.set_match_func(self.match_func) + completion.connect("match-selected", + self.on_completion_match) + completion.set_model(self.combobox.get_model()) + completion.set_text_column(0) + self.entry.set_completion(completion) + + # when user hits enter, close the dialog window + self.set_default_response(gtk.RESPONSE_OK) + self.entry.set_activates_default(True) + + if default_job_code: + self.entry.set_text(default_job_code) + + self.vbox.pack_start(task_hbox, False, False, padding = 6) + self.vbox.pack_start(self.job_code_hbox, False, False, padding=12) + + self.set_transient_for(parent_window) + self.show_all() + self.connect('response', self.on_job_code_resp) + + def match_func(self, completion, key, iter): + model = completion.get_model() + return model[iter][0].lower().startswith(self.entry.get_text().lower()) + + def on_completion_match(self, completion, model, iter): + self.entry.set_text(model[iter][0]) + self.entry.set_position(-1) + + def get_job_code(self): + return self.combobox.child.get_text() + + def on_job_code_resp(self, jc_dialog, response): + user_chose_code = False + if response == gtk.RESPONSE_OK: + user_chose_code = True + logger.debug("Job Code entered") + else: + logger.debug("Job Code not entered") + self.post_job_code_entry_callback(self, user_chose_code, self.get_job_code()) + + +class PreferencesDialog(): + """ + Dialog window to show Rapid Photo Downloader preferences. + + Is tightly integrated into main Rapid Photo Downloader window, i.e. + directly access members in class RapidApp. + """ + + def __init__(self, rapidapp): + + self.builder = gtk.Builder() + self.builder.add_from_file(paths.share_dir("glade3/prefs.ui")) + self.builder.connect_signals(self) + + self.dialog = self.preferencesdialog + self.widget = self.dialog + self.dialog.set_transient_for(rapidapp.rapidapp) + self.prefs = rapidapp.prefs + + rapidapp.preferences_dialog_displayed = True + + self.pref_dialog_startup = True + + self.rapidapp = rapidapp + + self._setup_tab_selector() + + self._setup_control_spacing() + + if metadatavideo.DOWNLOAD_VIDEO: + self.file_types = _("photos and videos") + else: + self.file_types = _("photos") + + self._setup_sample_names() + + # setup tabs + self._setup_photo_download_folder_tab() + self._setup_image_rename_tab() + self._setup_video_download_folder_tab() + self._setup_video_rename_tab() + self._setup_rename_options_tab() + self._setup_job_code_tab() + self._setup_device_tab() + self._setup_backup_tab() + self._setup_miscellaneous_tab() + self._setup_error_tab() + + if not metadatavideo.DOWNLOAD_VIDEO: + self.disable_video_controls() + + self.dialog.realize() + + #set the width of the left column for selecting values + #note: this must be called after self.dialog.realize(), or else the width calculation will fail + width_of_widest_sel_row = self.treeview.get_background_area(1, self.treeview_column)[2] + self.scrolled_window.set_size_request(width_of_widest_sel_row + 2, -1) + + #set the minimum width of the scolled window holding the photo rename table + if self.rename_scrolledwindow.get_vscrollbar(): + extra = self.rename_scrolledwindow.get_vscrollbar().allocation.width + 10 + else: + extra = 10 + self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) + + self.dialog.show() + + self.pref_dialog_startup = False + + def __getattr__(self, key): + """Allow builder widgets to be accessed as self.widgetname + """ + widget = self.builder.get_object(key) + if widget: # cache lookups + setattr(self, key, widget) + return widget + raise AttributeError(key) + + def on_preferencesdialog_destroy(self, widget): + """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" + logger.debug("Preference window closing") + + def _setup_tab_selector(self): + self.notebook.set_show_tabs(0) + self.model = gtk.ListStore(type("")) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.treeview_column = column + self.treeview.append_column(column) + self.treeview.props.model = self.model + for c in self.notebook.get_children(): + label = self.notebook.get_tab_label(c).get_text() + if not label.startswith("_"): + self.model.append( (label,) ) + + + # select the first value in the list store + self.treeview.set_cursor(0,column) + + def on_download_folder_filechooser_button_selection_changed(self, widget): + self.prefs.download_folder = widget.get_current_folder() + self.update_photo_download_folder_example() + + def on_video_download_folder_filechooser_button_selection_changed(self, widget): + self.prefs.video_download_folder = widget.get_current_folder() + self.update_video_download_folder_example() + + def on_backup_folder_filechooser_button_selection_changed(self, widget): + self.prefs.backup_location = widget.get_current_folder() + self.update_backup_example() + + def on_device_location_filechooser_button_selection_changed(self, widget): + self.prefs.device_location = widget.get_current_folder() + + def _setup_sample_names(self, use_dummy_data = False): + """ + If use_dummy_data is True, then samples will not attempt to get + data from actual download files + """ + job_code = self.prefs.most_recent_job_code() + if job_code is None: + job_code = _("Job Code") + self.downloads_today_tracker = DownloadsTodayTracker( + day_start = self.prefs.day_start, + downloads_today = self.prefs.downloads_today[1], + downloads_today_date = self.prefs.downloads_today[0]) + self.sequences = gn.Sequences(self.downloads_today_tracker, + self.prefs.stored_sequence_no) + + # get example photo and video data + if use_dummy_data: + self.sample_photo = None + else: + self.sample_photo = self.rapidapp.thumbnails.get_sample_file(rpdfile.FILE_TYPE_PHOTO) + if self.sample_photo is not None: + # try to load metadata from the file returned + # if it fails, give up with this sample file + if not self.sample_photo.load_metadata(): + self.sample_photo = None + else: + self.sample_photo.sequences = self.sequences + self.sample_photo.download_start_time = datetime.datetime.now() + + if self.sample_photo is None: + self.sample_photo = rpdfile.SamplePhoto(sequences=self.sequences) + + self.sample_photo.job_code = job_code + + self.sample_video = None + if metadatavideo.DOWNLOAD_VIDEO: + if not use_dummy_data: + self.sample_video = self.rapidapp.thumbnails.get_sample_file(rpdfile.FILE_TYPE_VIDEO) + if self.sample_video is not None: + self.sample_video.load_metadata() + self.sample_video.sequences = self.sequences + self.sample_video.download_start_time = datetime.datetime.now() + if self.sample_video is None: + self.sample_video = rpdfile.SampleVideo(sequences=self.sequences) + self.sample_video.job_code = job_code + + + + def _setup_control_spacing(self): + """ + set spacing of some but not all controls + """ + + self._setup_table_spacing(self.download_folder_table) + self._setup_table_spacing(self.video_download_folder_table) + self.download_folder_table.set_row_spacing(2, + hd.VERTICAL_CONTROL_SPACE) + self.video_download_folder_table.set_row_spacing(2, + hd.VERTICAL_CONTROL_SPACE) + self._setup_table_spacing(self.rename_example_table) + self._setup_table_spacing(self.video_rename_example_table) + self.devices_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + self.automation_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + + self._setup_table_spacing(self.backup_table) + self.backup_table.set_col_spacing(1, hd.NESTED_CONTROLS_SPACE) + self.backup_table.set_col_spacing(2, hd.CONTROL_LABEL_SPACE) + self._setup_table_spacing(self.compatibility_table) + self.compatibility_table.set_row_spacing(0, + hd.VERTICAL_CONTROL_LABEL_SPACE) + self._setup_table_spacing(self.error_table) + + + def _setup_table_spacing(self, table): + table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + table.set_col_spacing(1, hd.CONTROL_LABEL_SPACE) + + def _setup_subfolder_table(self): + self.subfolder_table = SubfolderTable(self, None) + self.subfolder_vbox.pack_start(self.subfolder_table) + self.subfolder_table.show_all() + + def _setup_video_subfolder_table(self): + self.video_subfolder_table = VideoSubfolderTable(self, None) + self.video_subfolder_vbox.pack_start(self.video_subfolder_table) + self.video_subfolder_table.show_all() + + def _setup_photo_download_folder_tab(self): + self.download_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download photos to")) + self.download_folder_filechooser_button.set_current_folder( + self.prefs.download_folder) + self.download_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.download_folder_filechooser_button.connect("selection-changed", + self.on_download_folder_filechooser_button_selection_changed) + + self.download_folder_table.attach( + self.download_folder_filechooser_button, + 2, 3, 2, 3, yoptions = gtk.SHRINK) + self.download_folder_filechooser_button.show() + + self._setup_subfolder_table() + self.update_photo_download_folder_example() + + def _setup_video_download_folder_tab(self): + self.video_download_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download videos to")) + self.video_download_folder_filechooser_button.set_current_folder( + self.prefs.video_download_folder) + self.video_download_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.video_download_folder_filechooser_button.connect("selection-changed", + self.on_video_download_folder_filechooser_button_selection_changed) + + self.video_download_folder_table.attach( + self.video_download_folder_filechooser_button, + 2, 3, 2, 3, yoptions = gtk.SHRINK) + self.video_download_folder_filechooser_button.show() + self._setup_video_subfolder_table() + self.update_video_download_folder_example() + + def _setup_image_rename_tab(self): + + self.rename_table = PhotoRenameTable(self, self.rename_scrolledwindow) + self.rename_table_vbox.pack_start(self.rename_table) + self.rename_table.show_all() + self._setup_photo_original_name() + self.update_photo_rename_example() + + def _setup_photo_original_name(self): + self.original_name_label.set_markup("<i>%s</i>" % self.sample_photo.display_name) + + def _setup_video_rename_tab(self): + + self.video_rename_table = VideoRenameTable(self, self.video_rename_scrolledwindow) + self.video_rename_table_vbox.pack_start(self.video_rename_table) + self.video_rename_table.show_all() + self._setup_video_original_name() + self.update_video_rename_example() + + def _setup_video_original_name(self): + if self.sample_video is not None: + self.video_original_name_label.set_markup("<i>%s</i>" % self.sample_video.display_name) + else: + self.video_original_name_label.set_markup("") + + def _setup_rename_options_tab(self): + + # sequence numbers + self.downloads_today_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 0)) + self.stored_number_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 1)) + self.downloads_today_entry.connect('changed', self.on_downloads_today_entry_changed) + self.stored_number_entry.connect('changed', self.on_stored_number_entry_changed) + v = self.rapidapp.downloads_today_tracker.get_and_maybe_reset_downloads_today() + self.downloads_today_entry.set_text(str(v)) + # make the displayed value of stored sequence no 1 more than actual value + # so as not to confuse the user + self.stored_number_entry.set_text(str(self.prefs.stored_sequence_no+1)) + self.sequence_vbox.pack_start(self.downloads_today_entry, expand=True, fill=True) + self.sequence_vbox.pack_start(self.stored_number_entry, expand=False) + self.downloads_today_entry.show() + self.stored_number_entry.show() + hour, minute = self.rapidapp.downloads_today_tracker.get_day_start() + self.hour_spinbutton.set_value(float(hour)) + self.minute_spinbutton.set_value(float(minute)) + + self.synchronize_raw_jpg_checkbutton.set_active( + self.prefs.synchronize_raw_jpg) + + #compatibility + self.strip_characters_checkbutton.set_active( + self.prefs.strip_characters) + + def _setup_job_code_tab(self): + self.job_code_liststore = gtk.ListStore(str) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + rentext.connect('edited', self.on_job_code_edited) + rentext .set_property('editable', True) + + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.job_code_treeview_column = column + self.job_code_treeview.append_column(column) + self.job_code_treeview.props.model = self.job_code_liststore + for code in self.prefs.job_codes: + self.job_code_liststore.append((code, )) + + # set multiple selections + self.job_code_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + + self.remove_all_job_code_button.set_image(gtk.image_new_from_stock( + gtk.STOCK_CLEAR, + gtk.ICON_SIZE_BUTTON)) + def _setup_device_tab(self): + + self.device_location_filechooser_button = gtk.FileChooserButton( + _("Select a folder containing %(file_types)s") % {'file_types':self.file_types}) + self.device_location_filechooser_button.set_current_folder( + self.prefs.device_location) + self.device_location_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + + self.device_location_filechooser_button.connect("selection-changed", + self.on_device_location_filechooser_button_selection_changed) + + self.devices2_table.attach(self.device_location_filechooser_button, + 1, 2, 1, 2, xoptions = gtk.EXPAND|gtk.FILL, yoptions = gtk.SHRINK) + self.device_location_filechooser_button.show() + self.autodetect_device_checkbutton.set_active( + self.prefs.device_autodetection) + self.autodetect_psd_checkbutton.set_active( + self.prefs.device_autodetection_psd) + + self.update_device_controls() + + + def _setup_backup_tab(self): + self.backup_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder in which to backup %(file_types)s") % {'file_types':self.file_types}) + self.backup_folder_filechooser_button.set_current_folder( + self.prefs.backup_location) + self.backup_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.backup_folder_filechooser_button.connect("selection-changed", + self.on_backup_folder_filechooser_button_selection_changed) + self.backup_table.attach(self.backup_folder_filechooser_button, + 3, 4, 8, 9, yoptions = gtk.SHRINK) + self.backup_folder_filechooser_button.show() + self.backup_identifier_entry.set_text(self.prefs.backup_identifier) + self.video_backup_identifier_entry.set_text(self.prefs.video_backup_identifier) + + #setup controls for manipulating sensitivity + self._backup_controls0 = [self.auto_detect_backup_checkbutton] + self._backup_controls1 = [self.backup_identifier_explanation_label, + self.backup_identifier_label, + self.backup_identifier_entry, + self.example_backup_path_label, + self.backup_example_label,] + self._backup_controls2 = [self.backup_location_label, + self.backup_folder_filechooser_button, + self.backup_location_explanation_label] + self._backup_controls = self._backup_controls0 + self._backup_controls1 + \ + self._backup_controls2 + + self._backup_video_controls = [self.video_backup_identifier_label, + self.video_backup_identifier_entry] + + #assign values to checkbuttons only when other controls + #have been setup, because their toggle signal is activated + #when a value is assigned + + self.backup_checkbutton.set_active(self.prefs.backup_images) + self.auto_detect_backup_checkbutton.set_active( + self.prefs.backup_device_autodetection) + self.update_backup_controls() + self.update_backup_example() + + def _setup_miscellaneous_tab(self): + self.auto_startup_checkbutton.set_active( + self.prefs.auto_download_at_startup) + self.auto_insertion_checkbutton.set_active( + self.prefs.auto_download_upon_device_insertion) + self.auto_unmount_checkbutton.set_active( + self.prefs.auto_unmount) + self.auto_exit_checkbutton.set_active( + self.prefs.auto_exit) + self.auto_exit_force_checkbutton.set_active( + self.prefs.auto_exit_force) + self.auto_delete_checkbutton.set_active( + self.prefs.auto_delete) + + self.update_misc_controls() + + + def _setup_error_tab(self): + if self.prefs.download_conflict_resolution == config.SKIP_DOWNLOAD: + self.skip_download_radiobutton.set_active(True) + else: + self.add_identifier_radiobutton.set_active(True) + + if self.prefs.backup_duplicate_overwrite: + self.backup_duplicate_overwrite_radiobutton.set_active(True) + else: + self.backup_duplicate_skip_radiobutton.set_active(True) + + + def update_example_file_name(self, display_table, rename_table, sample_rpd_file, generator, example_label): + if hasattr(self, display_table) and sample_rpd_file is not None: + sample_rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(sample_rpd_file.file_type) + sample_rpd_file.strip_characters = self.prefs.strip_characters + sample_rpd_file.initialize_problem() + name = generator.generate_name(sample_rpd_file) + else: + name = '' + + # since this is markup, escape it + text = "<i>%s</i>" % utilities.escape(name) + + if sample_rpd_file is not None: + if sample_rpd_file.has_problem(): + text += "\n" + # Translators: please do not modify or leave out html formatting tags like <i> and <b>. These are used to format the text the users sees + text += _("<i><b>Warning:</b> There is insufficient metadata to fully generate the name. Please use other renaming options.</i>") + + example_label.set_markup(text) + + def update_photo_rename_example(self): + """ + Displays example image name to the user + """ + generator = gn.PhotoName(self.prefs.image_rename) + self.update_example_file_name('rename_table', self.rename_table, + self.sample_photo, generator, + self.new_name_label) + + + def update_video_rename_example(self): + """ + Displays example video name to the user + """ + if self.sample_video is not None: + generator = gn.VideoName(self.prefs.video_rename) + else: + generator = None + self.update_example_file_name('video_rename_table', + self.video_rename_table, + self.sample_video, generator, + self.video_new_name_label) + + def update_download_folder_example(self, display_table, subfolder_table, + download_folder, sample_rpd_file, + generator, + example_download_path_label, + subfolder_warning_label): + """ + Displays example subfolder name(s) to the user + """ + + if hasattr(self, display_table) and sample_rpd_file is not None: + #~ subfolder_table.update_example_job_code() + sample_rpd_file.strip_characters = self.prefs.strip_characters + sample_rpd_file.initialize_problem() + path = generator.generate_name(sample_rpd_file) + else: + path = '' + + text = os.path.join(download_folder, path) + # since this is markup, escape it + path = utilities.escape(text) + + warning = "" + if sample_rpd_file is not None: + if sample_rpd_file.has_problem(): + warning = _("<i><b>Warning:</b> There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options.</i>" ) + + # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see + example_download_path_label.set_markup(_("<i>Example: %s</i>") % text) + subfolder_warning_label.set_markup(warning) + + def update_photo_download_folder_example(self): + if hasattr(self, 'subfolder_table'): + generator = gn.PhotoSubfolder(self.prefs.subfolder) + self.update_download_folder_example('subfolder_table', + self.subfolder_table, self.prefs.download_folder, + self.sample_photo, generator, + self.example_photo_download_path_label, + self.photo_subfolder_warning_label) + + def update_video_download_folder_example(self): + if hasattr(self, 'video_subfolder_table'): + if self.sample_video is not None: + generator = gn.VideoSubfolder(self.prefs.video_subfolder) + else: + generator = None + self.update_download_folder_example('video_subfolder_table', + self.video_subfolder_table, + self.prefs.video_download_folder, + self.sample_video, generator, + self.example_video_download_path_label, + self.video_subfolder_warning_label) + + def on_hour_spinbutton_value_changed(self, spinbutton): + hour = spinbutton.get_value_as_int() + minute = self.minute_spinbutton.get_value_as_int() + self.rapidapp.downloads_today_tracker.set_day_start(hour, minute) + self.on_downloads_today_entry_changed(self.downloads_today_entry) + + def on_minute_spinbutton_value_changed(self, spinbutton): + hour = self.hour_spinbutton.get_value_as_int() + minute = spinbutton.get_value_as_int() + self.rapidapp.downloads_today_tracker.set_day_start(hour, minute) + self.on_downloads_today_entry_changed(self.downloads_today_entry) + + def on_downloads_today_entry_changed(self, entry): + # do not update value if a download is occurring - it will mess it up! + if self.rapidapp.download_is_occurring(): + logger.info("Downloads today value not updated, as a download is currently occurring") + else: + v = entry.get_text() + try: + v = int(v) + except: + v = 0 + if v < 0: + v = 0 + self.rapidapp.downloads_today_tracker.reset_downloads_today(v) + self.rapidapp.refresh_downloads_today = True + self.update_photo_rename_example() + + def on_stored_number_entry_changed(self, entry): + # do not update value if a download is occurring - it will mess it up! + if self.rapidapp.download_is_occurring(): + logger.info("Stored number value not updated, as a download is currently occurring") + else: + v = entry.get_text() + try: + # the displayed value of stored sequence no 1 more than actual value + # so as not to confuse the user + v = int(v) - 1 + except: + v = 0 + if v < 0: + v = 0 + self.prefs.stored_sequence_no = v + self.update_photo_rename_example() + + def _update_subfolder_pref_on_error(self, new_pref_list): + self.prefs.subfolder = new_pref_list + + def _update_video_subfolder_pref_on_error(self, new_pref_list): + self.prefs.video_subfolder = new_pref_list + + + def check_subfolder_values_valid_on_exit(self, users_pref_list, update_pref_function, filetype, default_pref_list): + """ + Checks that the user has not entered in any inappropriate values + + If they have, filters out bad values and warns the user + """ + filtered, pref_list = filter_subfolder_prefs(users_pref_list) + if filtered: + logger.info("The %(filetype)s subfolder preferences had some unnecessary values removed.", {'filetype': filetype}) + if pref_list: + update_pref_function(pref_list) + else: + #Preferences list is now empty + msg = _("The %(filetype)s subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") % {'filetype': filetype} + sys.stderr.write(msg + "\n") + misc.run_dialog(PROGRAM_NAME, msg) + update_pref_function(self.prefs.get_default(default_pref_list)) + + def on_preferencesdialog_response(self, dialog, arg): + if arg == gtk.RESPONSE_HELP: + webbrowser.open("http://www.damonlynch.net/rapid/documentation") + else: + # arg==gtk.RESPONSE_CLOSE, or the user hit the 'x' to close the window + self.prefs.backup_identifier = self.backup_identifier_entry.get_property("text") + self.prefs.video_backup_identifier = self.video_backup_identifier_entry.get_property("text") + + #check subfolder preferences for bad values + self.check_subfolder_values_valid_on_exit(self.prefs.subfolder, self._update_subfolder_pref_on_error, _("photo"), "subfolder") + self.check_subfolder_values_valid_on_exit(self.prefs.video_subfolder, self._update_video_subfolder_pref_on_error, _("video"), "video_subfolder") + + self.dialog.destroy() + self.rapidapp.preferences_dialog_displayed = False + self.rapidapp.post_preference_change() + + + + + def on_add_job_code_button_clicked(self, button): + j = JobCodeDialog(parent_window = self.dialog, + job_codes = self.prefs.job_codes, + default_job_code = None, + post_job_code_entry_callback=self.add_job_code, + entry_only = True) + + def add_job_code(self, dialog, user_chose_code, job_code): + dialog.destroy() + if user_chose_code: + if job_code and job_code not in self.prefs.job_codes: + self.job_code_liststore.prepend((job_code, )) + self.update_job_codes() + selection = self.job_code_treeview.get_selection() + selection.unselect_all() + selection.select_path((0, )) + #scroll to the top + adjustment = self.job_code_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.lower) + + def on_remove_job_code_button_clicked(self, button): + """ remove selected job codes (can be multiple selection)""" + selection = self.job_code_treeview.get_selection() + model, selected = selection.get_selected_rows() + iters = [model.get_iter(path) for path in selected] + # only delete if a jobe code is selected + if iters: + no = len(iters) + path = None + for i in range(0, no): + iter = iters[i] + if i == no - 1: + path = model.get_path(iter) + model.remove(iter) + + # now that we removed the selection, play nice with + # the user and select the next item + selection.select_path(path) + + # if there was no selection that meant the user + # removed the last entry, so we try to select the + # last item + if not selection.path_is_selected(path): + row = path[0]-1 + # test case for empty lists + if row >= 0: + selection.select_path((row,)) + + self.update_job_codes() + self.update_photo_rename_example() + self.update_video_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def on_remove_all_job_code_button_clicked(self, button): + j = RemoveAllJobCodeDialog(self.dialog, self.remove_all_job_code) + + def remove_all_job_code(self, dialog, user_selected): + dialog.destroy() + if user_selected: + self.job_code_liststore.clear() + self.update_job_codes() + self.update_photo_rename_example() + self.update_video_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def on_job_code_edited(self, widget, path, new_text): + iter = self.job_code_liststore.get_iter(path) + self.job_code_liststore.set_value(iter, 0, new_text) + self.update_job_codes() + self.update_photo_rename_example() + self.update_video_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def update_job_codes(self): + """ update preferences with list of job codes""" + job_codes = [] + for row in self.job_code_liststore: + job_codes.append(row[0]) + self.prefs.job_codes = job_codes + + def on_auto_startup_checkbutton_toggled(self, checkbutton): + self.prefs.auto_download_at_startup = checkbutton.get_active() + + def on_auto_insertion_checkbutton_toggled(self, checkbutton): + self.prefs.auto_download_upon_device_insertion = checkbutton.get_active() + + def on_auto_unmount_checkbutton_toggled(self, checkbutton): + self.prefs.auto_unmount = checkbutton.get_active() + + + def on_auto_delete_checkbutton_toggled(self, checkbutton): + self.prefs.auto_delete = checkbutton.get_active() + + def on_auto_exit_checkbutton_toggled(self, checkbutton): + active = checkbutton.get_active() + self.prefs.auto_exit = active + if not active: + self.prefs.auto_exit_force = False + self.auto_exit_force_checkbutton.set_active(False) + self.update_misc_controls() + + def on_auto_exit_force_checkbutton_toggled(self, checkbutton): + self.prefs.auto_exit_force = checkbutton.get_active() + + def on_scan_metadata_checkbutton_toggled(self, checkbutton): + self.prefs.enable_previews = checkbutton.get_active() + + def on_autodetect_device_checkbutton_toggled(self, checkbutton): + self.prefs.device_autodetection = checkbutton.get_active() + self.update_device_controls() + + def on_autodetect_psd_checkbutton_toggled(self, checkbutton): + self.prefs.device_autodetection_psd = checkbutton.get_active() + + def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): + self.prefs.backup_duplicate_overwrite = widget.get_active() + + def on_backup_duplicate_skip_radiobutton_toggled(self, widget): + self.prefs.backup_duplicate_overwrite = not widget.get_active() + + def on_treeview_cursor_changed(self, tree): + path, column = tree.get_cursor() + self.notebook.set_current_page(path[0]) + + def on_synchronize_raw_jpg_checkbutton_toggled(self, check_button): + self.prefs.synchronize_raw_jpg = check_button.get_active() + + def on_strip_characters_checkbutton_toggled(self, check_button): + self.prefs.strip_characters = check_button.get_active() + self.update_photo_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def on_add_identifier_radiobutton_toggled(self, widget): + if widget.get_active(): + self.prefs.download_conflict_resolution = config.ADD_UNIQUE_IDENTIFIER + else: + self.prefs.download_conflict_resolution = config.SKIP_DOWNLOAD + + + def update_device_controls(self): + """ + Sets sensitivity of image device controls + """ + + controls = [self.device_location_explanation_label, + self.device_location_label, + self.device_location_filechooser_button] + + if self.prefs.device_autodetection: + for c in controls: + c.set_sensitive(False) + self.autodetect_psd_checkbutton.set_sensitive(True) + self.autodetect_image_devices_label.set_sensitive(True) + else: + for c in controls: + c.set_sensitive(True) + self.autodetect_psd_checkbutton.set_sensitive(False) + self.autodetect_image_devices_label.set_sensitive(False) + + if not self.pref_dialog_startup: + logger.debug("Resetting sample file photo and video files") + self._setup_sample_names(use_dummy_data = True) + self._setup_photo_original_name() + self.update_photo_download_folder_example() + self.update_photo_rename_example() + self.update_video_download_folder_example() + self._setup_video_original_name() + self.update_video_rename_example() + + def update_misc_controls(self): + """ + Sets sensitivity of miscillaneous controls + """ + + self.auto_exit_force_checkbutton.set_sensitive(self.prefs.auto_exit) + + + def update_backup_controls(self): + """ + Sets sensitivity of backup related widgets + """ + + if not self.backup_checkbutton.get_active(): + for c in self._backup_controls + self._backup_video_controls: + c.set_sensitive(False) + + else: + for c in self._backup_controls0: + c.set_sensitive(True) + self.update_backup_controls_auto() + + def update_backup_controls_auto(self): + """ + Sets sensitivity of subset of backup related widgets + """ + + if self.auto_detect_backup_checkbutton.get_active(): + for c in self._backup_controls1: + c.set_sensitive(True) + for c in self._backup_controls2: + c.set_sensitive(False) + for c in self._backup_video_controls: + c.set_sensitive(False) + if metadatavideo.DOWNLOAD_VIDEO: + for c in self._backup_video_controls: + c.set_sensitive(True) + else: + for c in self._backup_controls1: + c.set_sensitive(False) + for c in self._backup_controls2: + c.set_sensitive(True) + if metadatavideo.DOWNLOAD_VIDEO: + for c in self._backup_video_controls: + c.set_sensitive(False) + + def disable_video_controls(self): + """ + Disables video preferences if video downloading is disabled + (probably because the appropriate libraries to enable + video metadata extraction are not installed) + """ + controls = [self.example_video_filename_label, + self.original_video_filename_label, + self.new_video_filename_label, + self.video_new_name_label, + self.video_original_name_label, + self.video_rename_scrolledwindow, + self.video_folders_hbox, + self.video_backup_identifier_label, + self.video_backup_identifier_entry + ] + for c in controls: + c.set_sensitive(False) + + self.videos_cannot_be_downloaded_label.show() + self.folder_videos_cannot_be_downloaded_label.show() + self.folder_videos_cannot_be_downloaded_hbox.show() + + def on_auto_detect_backup_checkbutton_toggled(self, widget): + self.prefs.backup_device_autodetection = widget.get_active() + self.update_backup_controls_auto() + + def on_backup_checkbutton_toggled(self, widget): + self.prefs.backup_images = self.backup_checkbutton.get_active() + self.update_backup_controls() + + def on_backup_identifier_entry_changed(self, widget): + self.update_backup_example() + #~ self.prefs. + + def on_video_backup_identifier_entry_changed(self, widget): + self.update_backup_example() + + def on_backup_scan_folder_on_entry_changed(self, widget): + self.update_backup_example() + + def update_backup_example(self): + # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. + drive1 = os.path.join(config.MEDIA_LOCATION, _("externaldrive1")) + # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. + drive2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) + + path = os.path.join(drive1, self.backup_identifier_entry.get_text()) + path2 = os.path.join(drive2, self.backup_identifier_entry.get_text()) + path3 = os.path.join(drive2, self.video_backup_identifier_entry.get_text()) + path = utilities.escape(path) + path2 = utilities.escape(path2) + path3 = utilities.escape(path3) + if metadatavideo.DOWNLOAD_VIDEO: + example = "<i>%s</i>\n<i>%s</i>\n<i>%s</i>" % (path, path2, path3) + else: + example = "<i>%s</i>\n<i>%s</i>" % (path, path2) + self.example_backup_path_label.set_markup(example) diff --git a/rapid/prefsrapid.py b/rapid/prefsrapid.py new file mode 100644 index 0000000..81a425b --- /dev/null +++ b/rapid/prefsrapid.py @@ -0,0 +1,417 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import subprocess, os, datetime + +import prefs + +import preferencesdialog as pd +from generatenameconfig import * +import rpdfile + +import utilities +import config +__version__ = config.version + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +from gettext import gettext as _ + +def _get_default_location_legacy(options, ignore_missing_dir=False): + if ignore_missing_dir: + return utilities.get_full_path(options[0]) + for default in options: + path = utilities.get_full_path(default) + if os.path.isdir(path): + return path + return utilities.get_full_path('') + +def _get_default_location_XDG(dir_type): + proc = subprocess.Popen(['xdg-user-dir', dir_type], stdout=subprocess.PIPE) + output = proc.communicate()[0].strip() + return output + +def get_default_photo_location(ignore_missing_dir=False): + try: + return _get_default_location_XDG('PICTURES') + except: + return _get_default_location_legacy(config.DEFAULT_PHOTO_LOCATIONS, ignore_missing_dir) + +def get_default_video_location(ignore_missing_dir=False): + try: + return _get_default_location_XDG('VIDEOS') + except: + return _get_default_location_legacy(config.DEFAULT_VIDEO_LOCATIONS, ignore_missing_dir) + +def get_default_backup_photo_identifier(): + return os.path.split(get_default_photo_location(ignore_missing_dir = True))[1] + +def get_default_backup_video_identifier(): + return os.path.split(get_default_video_location(ignore_missing_dir = True))[1] + +def today(): + return datetime.date.today().strftime('%Y-%m-%d') + +class RapidPreferences(prefs.Preferences): + + defaults = { + "program_version": prefs.Value(prefs.STRING, ""), + "download_folder": prefs.Value(prefs.STRING, + get_default_photo_location()), + "video_download_folder": prefs.Value(prefs.STRING, + get_default_video_location()), + "subfolder": prefs.ListValue(prefs.STRING_LIST, DEFAULT_SUBFOLDER_PREFS), + "video_subfolder": prefs.ListValue(prefs.STRING_LIST, DEFAULT_VIDEO_SUBFOLDER_PREFS), + "image_rename": prefs.ListValue(prefs.STRING_LIST, [FILENAME, + NAME_EXTENSION, + ORIGINAL_CASE]), + "video_rename": prefs.ListValue(prefs.STRING_LIST, [FILENAME, + NAME_EXTENSION, + ORIGINAL_CASE]), + "device_autodetection": prefs.Value(prefs.BOOL, True), + "device_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "device_autodetection_psd": prefs.Value(prefs.BOOL, False), + "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']), + "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']), + "backup_images": prefs.Value(prefs.BOOL, False), + "backup_device_autodetection": prefs.Value(prefs.BOOL, True), + "backup_identifier": prefs.Value(prefs.STRING, + get_default_backup_photo_identifier()), + "video_backup_identifier": prefs.Value(prefs.STRING, + get_default_backup_video_identifier()), + "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "strip_characters": prefs.Value(prefs.BOOL, True), + "auto_download_at_startup": prefs.Value(prefs.BOOL, False), + "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), + "auto_unmount": prefs.Value(prefs.BOOL, False), + "auto_exit": prefs.Value(prefs.BOOL, False), + "auto_exit_force": prefs.Value(prefs.BOOL, False), + "auto_delete": prefs.Value(prefs.BOOL, False), + "download_conflict_resolution": prefs.Value(prefs.STRING, + config.SKIP_DOWNLOAD), + "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), + "display_selection": prefs.Value(prefs.BOOL, True), + "display_size_column": prefs.Value(prefs.BOOL, True), + "display_filename_column": prefs.Value(prefs.BOOL, False), + "display_type_column": prefs.Value(prefs.BOOL, True), + "display_path_column": prefs.Value(prefs.BOOL, False), + "display_device_column": prefs.Value(prefs.BOOL, False), + "display_preview_folders": prefs.Value(prefs.BOOL, True), + "show_log_dialog": prefs.Value(prefs.BOOL, False), + "day_start": prefs.Value(prefs.STRING, "03:00"), + "downloads_today": prefs.ListValue(prefs.STRING_LIST, [today(), '0']), + "stored_sequence_no": prefs.Value(prefs.INT, 0), + "job_codes": prefs.ListValue(prefs.STRING_LIST, [_('New York'), + _('Manila'), _('Prague'), _('Helsinki'), _('Wellington'), + _('Tehran'), _('Kampala'), _('Paris'), _('Berlin'), _('Sydney'), + _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'), + _('Jakarta'), _('Madrid'), _('Stockholm')]), + "synchronize_raw_jpg": prefs.Value(prefs.BOOL, False), + #~ "hpaned_pos": prefs.Value(prefs.INT, 0), + "vpaned_pos": prefs.Value(prefs.INT, 0), + "main_window_size_x": prefs.Value(prefs.INT, 0), + "main_window_size_y": prefs.Value(prefs.INT, 0), + "main_window_maximized": prefs.Value(prefs.INT, 0), + "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), + #~ "preview_zoom": prefs.Value(prefs.INT, zoom), + "enable_previews": prefs.Value(prefs.BOOL, True), + } + + def __init__(self): + prefs.Preferences.__init__(self, config.GCONF_KEY, self.defaults) + + + def get_downloads_today_tracker(self): + return DownloadsTodayTracker(downloads_today_date = self.downloads_today[0], + downloads_today = self.downloads_today[1], + day_start = self.day_start + ) + + def set_downloads_today_from_tracker(self, downloads_today_tracker): + self.downloads_today = downloads_today_tracker.downloads_today + self.day_start = downloads_today_tracker.day_start + + def get_sample_job_code(self): + if self.job_codes: + return self.job_codes[0] + else: + return '' + + def _get_pref_lists(self): + return (self.image_rename, self.subfolder, self.video_rename, + self.video_subfolder) + + def _pref_list_uses_component(self, pref_list, pref_component, offset): + for i in range(0, len(pref_list), 3): + if pref_list[i+offset] == pref_component: + return True + return False + + def must_synchronize_raw_jpg(self): + """Returns True if synchronize_raw_jpg is True and photo renaming + uses sequence values""" + if self.synchronize_raw_jpg: + for s in LIST_SEQUENCE_L1: + if self._pref_list_uses_component(self.image_rename, s, 1): + return True + return False + + def any_pref_uses_stored_sequence_no(self): + """Returns True if any of the pref lists contain a stored sequence no""" + for pref_list in self._get_pref_lists(): + if self._pref_list_uses_component(pref_list, STORED_SEQ_NUMBER, 1): + return True + return False + + def any_pref_uses_session_sequece_no(self): + """Returns True if any of the pref lists contain a session sequence no""" + for pref_list in self._get_pref_lists(): + if self._pref_list_uses_component(pref_list, SESSION_SEQ_NUMBER, 1): + return True + return False + + def any_pref_uses_sequence_letter_value(self): + """Returns True if any of the pref lists contain a sequence letter""" + for pref_list in self._get_pref_lists(): + if self._pref_list_uses_component(pref_list, SEQUENCE_LETTER, 1): + return True + return False + + def reset(self): + """ + resets all preferences to default values + """ + + prefs.Preferences.reset(self) + self.program_version = __version__ + + + def pref_uses_job_code(self, pref_list): + """ Returns True if the particular preferences contains a job code""" + for i in range(0, len(pref_list), 3): + if pref_list[i] == JOB_CODE: + return True + return False + + def any_pref_uses_job_code(self): + """ Returns True if any of the preferences contain a job code""" + for pref_list in self._get_pref_lists(): + if self.pref_uses_job_code(pref_list): + return True + return False + + def most_recent_job_code(self): + if len(self.job_codes) > 0: + return self.job_codes[0] + else: + return None + + def get_pref_lists_by_file_type(self, file_type): + """ + Returns tuple of subfolder and file rename pref lists for the given + file type + """ + if file_type == rpdfile.FILE_TYPE_PHOTO: + return (self.subfolder, self.image_rename) + else: + return (self.video_subfolder, self.video_rename) + + def get_download_folder_for_file_type(self, file_type): + """ + Returns the download folder for the given file type + """ + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.download_folder + else: + return self.video_download_folder + + +class DownloadsTodayTracker: + """ + Handles tracking the number of downloads undertaken on any one day. + + When a day starts is flexible. See http://damonlynch.net/rapid/documentation/#renameoptions + """ + def __init__(self, downloads_today_date, downloads_today, day_start): + self.day_start = day_start # string + self.downloads_today = [downloads_today_date, str(downloads_today)] # two strings + + def get_and_maybe_reset_downloads_today(self): + v = self.get_downloads_today() + if v <= 0: + self.reset_downloads_today() + return v + + def get_downloads_today(self): + """Returns the preference value for the number of downloads performed today + + If value is less than zero, that means the date has changed""" + + hour, minute = self.get_day_start() + try: + adjusted_today = datetime.datetime.strptime("%s %s:%s" % (self.downloads_today[0], hour, minute), "%Y-%m-%d %H:%M") + except: + logger.critical("Failed to calculate date adjustment. Download today values appear to be corrupted: %s %s:%s", + self.downloads_today[0], hour, minute) + adjusted_today = None + + now = datetime.datetime.today() + + if adjusted_today is None: + return -1 + + if now < adjusted_today : + try: + return int(self.downloads_today[1]) + except ValueError: + logger.error("Invalid Downloads Today value. Resetting value to zero.") + self.get_downloads_today(self.downloads_today[0] , 0) + return 0 + else: + return -1 + + def get_raw_downloads_today(self): + """ + Gets value without changing it in any way, except to check for type convesion error. + If there is an error, then the value is reset + """ + try: + return int(self.downloads_today[1]) + except ValueError: + logger.critical("Downloads today value is corrupted: %s", self.downloads_today[1]) + self.downloads_today[1] = '0' + return 0 + + def set_raw_downloads_today_from_int(self, downloads_today): + self.downloads_today[1] = str(downloads_today) + + def set_raw_downloads_today_date(self, downloads_today_date): + self.downloads_today[0] = downloads_today_date + + def get_raw_downloads_today_date(self): + return self.downloads_today[0] + + def get_raw_day_start(self): + """ + Gets value without changing it in any way + """ + return self.day_start + + def get_day_start(self): + try: + t1, t2 = self.day_start.split(":") + return (int(t1), int(t2)) + except ValueError: + logger.error("'Start of day' preference value %s is corrupted. Resetting to midnight", self.day_start) + self.day_start = "0:0" + return 0, 0 + + def increment_downloads_today(self): + """ returns true if day changed """ + v = self.get_downloads_today() + if v >= 0: + self.set_downloads_today(self.downloads_today[0], v + 1) + return False + else: + self.reset_downloads_today(1) + return True + + def reset_downloads_today(self, value=0): + now = datetime.datetime.today() + hour, minute = self.get_day_start() + t = datetime.time(hour, minute) + if now.time() < t: + date = today() + else: + d = datetime.datetime.today() + datetime.timedelta(days=1) + date = d.strftime(('%Y-%m-%d')) + + self.set_downloads_today(date, value) + + def set_downloads_today(self, date, value=0): + self.downloads_today = [date, str(value)] + + def set_day_start(self, hour, minute): + self.day_start = "%s:%s" % (hour, minute) + + def log_vals(self): + logger.info("Date %s Value %s Day start %s", self.downloads_today[0], self.downloads_today[1], self.day_start) + + + +def check_prefs_for_validity(prefs): + """ + Checks preferences for validity (called at program startup) + + Returns tuple with two values: + 1. true if the passed in preferences are valid, else returns False + 2. message if prefs are invalid + """ + + + msg = '' + valid = True + tests = ((prefs.image_rename, pd.PhotoNamePrefs), + (prefs.subfolder, pd.PhotoSubfolderPrefs), + (prefs.video_rename, pd.VideoNamePrefs), + (prefs.video_subfolder, pd.VideoSubfolderPrefs)) + for pref, pref_widgets in tests: + p = pref_widgets(pref) + try: + p.check_prefs_for_validity() + except pd.PrefError as e: + valid = False + msg += e.msg + "\n" + + return (valid, msg) + +def insert_pref_lists(prefs, rpd_file): + """ + Convenience function to insert subfolder and file rename pref_lists for + the given file type. + + Returns the modified rpd_file + """ + subfolder_pref_list, name_pref_list = prefs.get_pref_lists_by_file_type(rpd_file.file_type) + rpd_file.subfolder_pref_list = subfolder_pref_list + rpd_file.name_pref_list = name_pref_list + return rpd_file + +def format_pref_list_for_pretty_print(pref_list): + """ returns a string useful for printing the preferences""" + + v = '' + + for i in range(0, len(pref_list), 3): + if (pref_list[i+1] or pref_list[i+2]): + c = ':' + else: + c = '' + s = "%s%s " % (pref_list[i], c) + + if pref_list[i+1]: + s = "%s%s" % (s, pref_list[i+1]) + if pref_list[i+2]: + s = "%s (%s)" % (s, pref_list[i+2]) + v += s + "\n" + return v + + diff --git a/rapid/problemnotification.py b/rapid/problemnotification.py index 8dc07d0..6238847 100755 --- a/rapid/problemnotification.py +++ b/rapid/problemnotification.py @@ -19,9 +19,7 @@ import sys import types -from common import Configi18n -global _ -_ = Configi18n._ +from gettext import gettext as _ # components diff --git a/rapid/rapid.py b/rapid/rapid.py index f41c79f..df60790 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: latin1 -*- -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch <damonlynch@gmail.com> +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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 @@ -17,19 +17,8 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -#needed for python 2.5, unneeded for python 2.6 -from __future__ import with_statement -import sys -import os -import shutil -import time -import datetime -import atexit import tempfile -import types -import webbrowser -import operator import dbus import dbus.bus @@ -37,5346 +26,2461 @@ import dbus.service from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) -from threading import Thread, Lock -from thread import error as thread_error -from thread import get_ident +from optparse import OptionParser +import gtk import gtk.gdk as gdk -import pango -import gobject - -try: - import gio - import glib - using_gio = True -except ImportError: - import gnomevfs - using_gio = False - -import prefs -import paths -import gnomeglade -from optparse import OptionParser +import webbrowser + +import sys, time, types, os, datetime +import gobject, pango, cairo, array, pangocairo, gio import pynotify -import idletube as tube +from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr +from ctypes import c_int, c_bool, c_char -import config +import logging +logger = log_to_stderr() -from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ - STATUS_DOWNLOADED_WITH_WARNING, \ - STATUS_DOWNLOAD_FAILED, \ - STATUS_DOWNLOAD_PENDING, \ - STATUS_BACKUP_PROBLEM, \ - STATUS_NOT_DOWNLOADED, \ - STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ - STATUS_WARNING - -import common -import misc -import higdefaults as hd +# Rapid Photo Downloader modules -from media import getDefaultPhotoLocation, getDefaultVideoLocation, \ - getDefaultBackupPhotoIdentifier, \ - getDefaultBackupVideoIdentifier - -import ValidatedEntry +import rpdfile -from media import CardMedia - -import media - -import metadata -import videometadata -from videometadata import DOWNLOAD_VIDEO - -import renamesubfolderprefs as rn import problemnotification as pn +import thumbnail as tn +import rpdmultiprocessing as rpdmp -import tableplusminus as tpm - -__version__ = config.version +import preferencesdialog +import prefsrapid -try: - import pygtk - pygtk.require("2.0") -except: - pass -try: - import gtk - import gtk.glade -except: - sys.exit(1) - -try: - from dropshadow import image_to_pixbuf, pixbuf_to_image, DropShadow - DROP_SHADOW = True -except: - DROP_SHADOW = False - - -#~ DROP_SHADOW = False # for testing - -from common import Configi18n -global _ -_ = Configi18n._ - -#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html -PROGRAM_NAME = _('Rapid Photo Downloader') +import tableplusminus as tpm +import generatename as gn -TINY_SCREEN = gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT -#~ TINY_SCREEN = True +import downloadtracker -def today(): - return datetime.date.today().strftime('%Y-%m-%d') +from metadatavideo import DOWNLOAD_VIDEO +import metadataphoto +import metadatavideo +import scan as scan_process +import copyfiles +import subfolderfile +import errorlog -def cmd_line(msg): - if verbose: - print msg +import device as dv +import utilities -exiting = False +import config +__version__ = config.version -def updateDisplay(display_queue): +import paths - try: - if display_queue.size() != 0: - call, args = display_queue.get() - if not exiting: - call(*args) -# else do not update display - else: - sys.stderr.write("Empty display queue!\n") - return True - - except tube.EOInformation: - for w in workers.getStartedWorkers(): - w.join() +import gettext +gettext.bindtextdomain(config.APP_NAME) +gettext.textdomain(config.APP_NAME) - gtk.main_quit() - - return False +from gettext import gettext as _ -class Queue(tube.Tube): - def __init__(self, maxSize = config.MAX_NO_READERS): - tube.Tube.__init__(self, maxSize) +from utilities import format_size_for_user +from utilities import register_iconsets - def setMaxSize(self, maxSize): - self.maxsize = maxSize +from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ + STATUS_DOWNLOADED_WITH_WARNING, \ + STATUS_DOWNLOAD_FAILED, \ + STATUS_DOWNLOAD_PENDING, \ + STATUS_BACKUP_PROBLEM, \ + STATUS_NOT_DOWNLOADED, \ + STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ + STATUS_WARNING + +DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM] -# Module wide values - -# set up thesse variable in global name space, and initialize with proper -# values later -# this is ugly but I don't know a better way :( +#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html +PROGRAM_NAME = _('Rapid Photo Downloader') +__version__ = config.version -display_queue = Queue() -media_collection_treeview = selection_hbox = log_dialog = None +def date_time_human_readable(date, with_line_break=True): + if with_line_break: + return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} + else: + return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} + +def date_time_subseconds_human_readable(date, subseconds): + return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ + {'date':date.strftime("%x"), + 'hour':date.strftime("%H"), + 'minute':date.strftime("%M"), + 'second':date.strftime("%S"), + 'subsecond': subseconds} -job_code = None -need_job_code_for_renaming = False -class ThreadManager: +class DeviceCollection(gtk.TreeView): """ - Manages the threads that actually download photos and videos + TreeView display of devices and how many files have been copied, shown + immediately under the menu in the main application window. """ - _workers = [] - - - def append(self, w): - self._workers.append(w) - - def __getitem__(self, i): - return self._workers[i] - - def __len__(self): - return len(self._workers) - - def disableWorker(self, thread_id): - """ - set so a worker will not run, or if it is running, make it quit and therefore complete - """ - - self._workers[thread_id].manuallyDisabled = True - if self._workers[thread_id].hasStarted: - self._workers[thread_id].quit() - - else: - self._workers[thread_id].doNotStart = True - - def _isReadyToStart(self, w): - """ - Returns True if the worker is ready to start - and has not been disabled - """ - return not w.hasStarted and not w.doNotStart and not w.manuallyDisabled + def __init__(self, parent_app): + + self.parent_app = parent_app + # device icon & name, size of images on the device (human readable), + # copy progress (%), copy text, eject button (None if irrelevant), + # process id + self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str, + gtk.gdk.Pixbuf, int) + self.map_process_to_row = {} + self.devices_by_scan_pid = {} + + gtk.TreeView.__init__(self, self.liststore) - def _isReadyToDownload(self, w): - return w.scanComplete and not w.downloadStarted and not w.doNotStart and w.isAlive() and not w.manuallyDisabled - - def _isScanning(self, w): - return w.isAlive() and w.hasStarted and not w.scanComplete and not w.manuallyDisabled - - def _isDownloading(self, w): - return w.downloadStarted and w.isAlive() and not w.downloadComplete + self.props.enable_search = False + # make it impossible to select a row + selection = self.get_selection() + selection.set_mode(gtk.SELECTION_NONE) - def _isPaused(self, w): - return w.downloadStarted and not w.running and not w.downloadComplete and not w.manuallyDisabled and w.isAlive() - def _isFinished(self, w): - """ - Returns True if the worker has finished running + # Device refers to a thing like a camera, memory card in its reader, + # external hard drive, Portable Storage Device, etc. + column0 = gtk.TreeViewColumn(_("Device")) + pixbuf_renderer = gtk.CellRendererPixbuf() + text_renderer = gtk.CellRendererText() + text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE + text_renderer.set_fixed_size(160, -1) + eject_renderer = gtk.CellRendererPixbuf() + column0.pack_start(pixbuf_renderer, expand=False) + column0.pack_start(text_renderer, expand=True) + column0.pack_end(eject_renderer, expand=False) + column0.add_attribute(pixbuf_renderer, 'pixbuf', 0) + column0.add_attribute(text_renderer, 'text', 1) + column0.add_attribute(eject_renderer, 'pixbuf', 5) + self.append_column(column0) - It does not signify it finished a download - """ - return (w.hasStarted and not w.isAlive()) or w.manuallyDisabled - - def completedDownload(self, w): - return w.completedDownload - - def firstWorkerReadyToStart(self): - for w in self._workers: - if self._isReadyToStart(w): - return w - return None - - def firstWorkerReadyToDownload(self): - for w in self._workers: - if self._isReadyToDownload(w): - return w - return None - - def startWorkers(self): - for w in self.getReadyToStartWorkers(): - #for some reason, very occassionally a thread that has been started shows up in this list, so must filter them out - if not w.isAlive(): - w.start() - - def quitAllWorkers(self): - global exiting - exiting = True - for w in self._workers: - w.quit() - - def getWorkers(self): - for w in self._workers: - yield w - - def getNonFinishedWorkers(self): - for w in self._workers: - if not self._isFinished(w): - yield w - - def getStartedWorkers(self): - for w in self._workers: - if w.hasStarted: - yield w - - def getReadyToStartWorkers(self): - for w in self._workers: - if self._isReadyToStart(w): - yield w - - def getReadyToDownloadWorkers(self): - for w in self._workers: - if self._isReadyToDownload(w): - yield w - - def getNotDownloadingWorkers(self): - for w in self._workers: - if w.hasStarted and not w.downloadStarted: - yield w - - def getNotDownloadingAndNotFinishedWorkers(self): - for w in self._workers: - if w.hasStarted and not w.downloadStarted and not self._isFinished(w): - yield w - - - def noReadyToStartWorkers(self): - n = 0 - for w in self._workers: - if self._isReadyToStart(w): - n += 1 - return n - - def noScanningWorkers(self): - n = 0 - for w in self._workers: - if self._isScanning(w): - n += 1 - return n - - def getScanningWorkers(self): - for w in self._workers: - if self._isScanning(w): - yield w - - def scanComplete(self, threads): - """ - Returns True only if the list of threads have completed their scan - """ - for thread_id in threads: - if not self[thread_id].scanComplete: - return False - return True - - def noReadyToDownloadWorkers(self): - n = 0 - for w in self._workers: - if self._isReadyToDownload(w): - n += 1 - return n - - def getRunningWorkers(self): - for w in self._workers: - if w.hasStarted and w.isAlive(): - yield w - - def getDownloadingWorkers(self): - for w in self._workers: - if self._isDownloading(w): - yield w - - def getPausedDownloadingWorkers(self): - for w in self._workers: - if self._isPaused(w): - yield w - - def getWaitingForJobCodeWorkers(self): - for w in self._workers: - if w.waitingForJobCode: - yield w - - def getAutoStartWorkers(self): - for w in self._workers: - if w.autoStart: - yield w - - def getFinishedWorkers(self): - for w in self._workers: - if self._isFinished(w): - yield w - - def noDownloadingWorkers(self): - i = 0 - for w in self._workers: - if self._isDownloading(w): - i += 1 - return i - - def noRunningWorkers(self): - i = 0 - for w in self._workers: - if w.hasStarted and w.isAlive(): - i += 1 - return i - - def noPausedWorkers(self): - i = 0 - for w in self._workers: - if self._isPaused(w): - i += 1 - return i + # Size refers to the total size of images on the device, typically in + # MB or GB + column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2) + self.append_column(column1) - def getNextThread_id(self): - return len(self._workers) + column2 = gtk.TreeViewColumn(_("Download Progress"), + gtk.CellRendererProgress(), + value=3, + text=4) + self.append_column(column2) + self.show_all() - def printWorkerStatus(self, worker=None): - if worker: - l = [worker] - else: - l = range(len(self._workers)) - for i in l: - print "\nThread %i\n=======\n" % i - w = self._workers[i] - print "Volume / source:", w.cardMedia.prettyName(limit=0) - print "Do not start:", w.doNotStart - print "Started:", w.hasStarted - print "Running:", w.running - print "Scan completed:", w.scanComplete - print "Download started:", w.downloadStarted - print "Download completed:", w.downloadComplete - print "Finished:", self._isFinished(w) - print "Alive:", w.isAlive() - print "Manually disabled:", w.manuallyDisabled, "\n" - - + icontheme = gtk.icon_theme_get_default() + try: + self.eject_pixbuf = icontheme.load_icon('media-eject', 16, + gtk.ICON_LOOKUP_USE_BUILTIN) + except: + self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file( + paths.share_dir('glade3/media-eject.png')) + + self.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.connect('button-press-event', self.button_clicked) -workers = ThreadManager() - -class RapidPreferences(prefs.Preferences): - if TINY_SCREEN: - zoom = 120 - else: - zoom = config.MIN_THUMBNAIL_SIZE * 2 - - defaults = { - "program_version": prefs.Value(prefs.STRING, ""), - "download_folder": prefs.Value(prefs.STRING, - getDefaultPhotoLocation()), - "video_download_folder": prefs.Value(prefs.STRING, - getDefaultVideoLocation()), - "subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_SUBFOLDER_PREFS), - "video_subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_VIDEO_SUBFOLDER_PREFS), - "image_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, - rn.NAME_EXTENSION, - rn.ORIGINAL_CASE]), - "video_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, - rn.NAME_EXTENSION, - rn.ORIGINAL_CASE]), - "device_autodetection": prefs.Value(prefs.BOOL, True), - "device_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), - "device_autodetection_psd": prefs.Value(prefs.BOOL, False), - "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']), - "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']), - "backup_images": prefs.Value(prefs.BOOL, False), - "backup_device_autodetection": prefs.Value(prefs.BOOL, True), - "backup_identifier": prefs.Value(prefs.STRING, - getDefaultBackupPhotoIdentifier()), - "video_backup_identifier": prefs.Value(prefs.STRING, - getDefaultBackupVideoIdentifier()), - "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), - "strip_characters": prefs.Value(prefs.BOOL, True), - "auto_download_at_startup": prefs.Value(prefs.BOOL, False), - "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), - "auto_unmount": prefs.Value(prefs.BOOL, False), - "auto_exit": prefs.Value(prefs.BOOL, False), - "auto_delete": prefs.Value(prefs.BOOL, False), - "download_conflict_resolution": prefs.Value(prefs.STRING, - config.SKIP_DOWNLOAD), - "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), - "display_selection": prefs.Value(prefs.BOOL, True), - "display_size_column": prefs.Value(prefs.BOOL, True), - "display_filename_column": prefs.Value(prefs.BOOL, False), - "display_type_column": prefs.Value(prefs.BOOL, True), - "display_path_column": prefs.Value(prefs.BOOL, False), - "display_device_column": prefs.Value(prefs.BOOL, False), - "display_preview_folders": prefs.Value(prefs.BOOL, True), - "show_log_dialog": prefs.Value(prefs.BOOL, False), - "day_start": prefs.Value(prefs.STRING, "03:00"), - "downloads_today": prefs.ListValue(prefs.STRING_LIST, [today(), '0']), - "stored_sequence_no": prefs.Value(prefs.INT, 0), - "job_codes": prefs.ListValue(prefs.STRING_LIST, [_('New York'), - _('Manila'), _('Prague'), _('Helsinki'), _('Wellington'), - _('Tehran'), _('Kampala'), _('Paris'), _('Berlin'), _('Sydney'), - _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'), - _('Jakarta'), _('Madrid'), _('Stockholm')]), - "synchronize_raw_jpg": prefs.Value(prefs.BOOL, False), - "hpaned_pos": prefs.Value(prefs.INT, 0), - "vpaned_pos": prefs.Value(prefs.INT, 0), - "main_window_size_x": prefs.Value(prefs.INT, 0), - "main_window_size_y": prefs.Value(prefs.INT, 0), - "main_window_maximized": prefs.Value(prefs.INT, 0), - "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), - "preview_zoom": prefs.Value(prefs.INT, zoom), - } - - def __init__(self): - prefs.Preferences.__init__(self, config.GCONF_KEY, self.defaults) - - def getAndMaybeResetDownloadsToday(self): - v = self.getDownloadsToday() - if v <= 0: - self.resetDownloadsToday() - return v - def getDownloadsToday(self): - """Returns the preference value for the number of downloads performed today + def add_device(self, process_id, device, progress_bar_text = ''): - If value is less than zero, that means the date has changed""" - - hour, minute = self.getDayStart() - adjustedToday = datetime.datetime.strptime("%s %s:%s" % (self.downloads_today[0], hour, minute), "%Y-%m-%d %H:%M") + # add the row, and get a temporary pointer to the row + size_files = '' + progress = 0.0 - now = datetime.datetime.today() - - if now < adjustedToday : - try: - return int(self.downloads_today[1]) - except ValueError: - sys.stderr.write(_("Invalid Downloads Today value.\n")) - sys.stderr.write(_("Resetting value to zero.\n")) - self.setDownloadsToday(self.downloads_today[0] , 0) - return 0 + if device.mount is None: + eject = None else: - return -1 - - def setDownloadsToday(self, date, value=0): - self.downloads_today = [date, str(value)] + eject = self.eject_pixbuf - def incrementDownloadsToday(self): - """ returns true if day changed """ - v = self.getDownloadsToday() - if v >= 0: - self.setDownloadsToday(self.downloads_today[0] , v + 1) - return False - else: - self.resetDownloadsToday(1) - return True - - def resetDownloadsToday(self, value=0): - now = datetime.datetime.today() - hour, minute = self.getDayStart() - t = datetime.time(hour, minute) - if now.time() < t: - date = today() - else: - d = datetime.datetime.today() + datetime.timedelta(days=1) - date = d.strftime(('%Y-%m-%d')) + self.devices_by_scan_pid[process_id] = device - self.setDownloadsToday(date, value) + iter = self.liststore.append((device.get_icon(), + device.get_name(), + size_files, + progress, + progress_bar_text, + eject, + process_id)) - def setDayStart(self, hour, minute): - self.day_start = "%s:%s" % (hour, minute) - - def getDayStart(self): - try: - t1, t2 = self.day_start.split(":") - return (int(t1), int(t2)) - except ValueError: - sys.stderr.write(_("'Start of day' preference value is corrupted.\n")) - sys.stderr.write(_("Resetting to midnight.\n")) - self.day_start = "0:0" - return 0, 0 - - def getSampleJobCode(self): - if self.job_codes: - return self.job_codes[0] - else: - return '' - - def reset(self): - """ - resets all preferences to default values - """ + self._set_process_map(process_id, iter) - prefs.Preferences.reset(self) - self.program_version = __version__ - -class ImageRenameTable(tpm.TablePlusMinus): - - def __init__(self, parentApp, adjustScrollWindow): - - tpm.TablePlusMinus.__init__(self, 1, 3) - self.parentApp = parentApp - self.adjustScrollWindow = adjustScrollWindow - if not hasattr(self, "errorTitle"): - self.errorTitle = _("Error in Photo Rename preferences") - - self.table_type = self.errorTitle[len("Error in "):] - self.i = 0 - - if adjustScrollWindow: - self.scrollBar = self.adjustScrollWindow.get_vscrollbar() - #this next line does not work on early versions of pygtk :( - self.scrollBar.connect('visibility-notify-event', self.scrollbar_visibility_change) - self.connect("size-request", self.size_adjustment) - self.connect("add", self.size_adjustment) - self.connect("remove", self.size_adjustment) - - # get scrollbar thickness from parent app scrollbar - very hackish, but what to do?? - self.bump = 16# self.parentApp.parentApp.image_scrolledwindow.get_hscrollbar().allocation.height - self.haveVerticalScrollbar = False - - # vbar is '1' if there is not vertical scroll bar - # if there is a vertical scroll bar, then it will have a the width of the bar - #self.vbar = self.adjustScrollWindow.get_vscrollbar().allocation.width + # adjust scrolled window height, based on row height and number of ready to start downloads - self.getParentAppPrefs() - self.getPrefsFactory() - self.prefsFactory.setDownloadStartTime(datetime.datetime.now()) + # please note, at program startup, self.row_height() will be less than it will be when already running + # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program + # (with one card at startup), it could be 21 + row_height = self.get_background_area(0, self.get_column(0))[3] + 1 + height = (len(self.map_process_to_row) + 1) * row_height + self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height) - try: - self.prefsFactory.checkPrefsForValidity() - - except (rn.PrefValueInvalidError, rn.PrefLengthError, - rn.PrefValueKeyComboError, rn.PrefKeyError), e: - - sys.stderr.write(self.errorTitle + "\n") - sys.stderr.write(_("Sorry,these preferences contain an error:\n")) - sys.stderr.write(self.prefsFactory.formatPreferencesForPrettyPrint() + "\n") - - # the preferences were invalid - # reset them to their default - - self.prefList = self.prefsFactory.defaultPrefs - self.getPrefsFactory() - self.updateParentAppPrefs() - - msg = "%s.\n" % e - msg += _("Resetting to default values." + "\n") - sys.stderr.write(msg) - - - misc.run_dialog(self.errorTitle, msg, - parentApp, - gtk.MESSAGE_ERROR) - - for row in self.prefsFactory.getWidgetsBasedOnPreferences(): - self.append(row) - - def updatePreferences(self): - prefList = [] - for row in self.pm_rows: - for col in range(self.pm_noColumns): - widget = row[col] - if widget: - name = widget.get_name() - if name == 'GtkComboBox': - value = widget.get_active_text() - elif name == 'GtkEntry': - value = widget.get_text() - else: - sys.stderr.write("Program error: Unknown preference widget!") - value = '' - else: - value = '' - prefList.append(value) - - self.prefList = prefList - self.updateParentAppPrefs() - self.prefsFactory.prefList = prefList - self.updateExample() - - - def scrollbar_visibility_change(self, widget, event): - if event.state == gdk.VISIBILITY_UNOBSCURED: - self.haveVerticalScrollbar = True - self.adjustScrollWindow.set_size_request(self.adjustScrollWindow.allocation.width + self.bump, -1) - - - def size_adjustment(self, widget, arg2): + def update_device(self, process_id, total_size_files): """ - Adjust scrolledwindow width in preferences dialog to reflect width of image rename table - - The algorithm is complicated by the need to take into account the presence of a vertical scrollbar, - which might be added as the user adds more rows - - The pygtk code behaves inconsistently depending on the pygtk version + Updates the size of the photos and videos on the device, displayed to the user """ - - if self.adjustScrollWindow: - self.haveVerticalScrollbar = self.scrollBar.allocation.width > 1 or self.haveVerticalScrollbar - if not self.haveVerticalScrollbar: - if self.allocation.width > self.adjustScrollWindow.allocation.width: - self.adjustScrollWindow.set_size_request(self.allocation.width, -1) - else: - if self.allocation.width > self.adjustScrollWindow.allocation.width - self.bump: - self.adjustScrollWindow.set_size_request(self.allocation.width + self.bump, -1) - self.bump = 0 - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.image_rename - - - def getPrefsFactory(self): - self.prefsFactory = rn.ImageRenamePreferences(self.prefList, self, - sequences = sequences) - - def updateParentAppPrefs(self): - self.parentApp.prefs.image_rename = self.prefList - - def updateExampleJobCode(self): - job_code = self.parentApp.prefs.getSampleJobCode() - if not job_code: - job_code = _('Job code') - self.prefsFactory.setJobCode(job_code) - - def updateExample(self): - self.parentApp.updateImageRenameExample() - - def getDefaultRow(self): - return self.prefsFactory.getDefaultRow() - - def on_combobox_changed(self, widget, rowPosition): - - for col in range(self.pm_noColumns): - if self.pm_rows[rowPosition][col] == widget: - break - selection = [] - for i in range(col + 1): - # ensure it is a combo box we are getting the value from - w = self.pm_rows[rowPosition][i] - name = w.get_name() - if name == 'GtkComboBox': - selection.append(w.get_active_text()) - else: - selection.append(w.get_text()) - - for i in range(col + 1, self.pm_noColumns): - selection.append('') + if process_id in self.map_process_to_row: + iter = self._get_process_map(process_id) + self.liststore.set_value(iter, 2, total_size_files) + else: + logger.critical("This device is unknown") - if col <> (self.pm_noColumns - 1): - widgets = self.prefsFactory.getWidgetsBasedOnUserSelection(selection) + def get_device(self, process_id): + return self.devices_by_scan_pid.get(process_id) + + def remove_device(self, process_id): + if process_id in self.map_process_to_row: + iter = self._get_process_map(process_id) + self.liststore.remove(iter) + del self.map_process_to_row[process_id] + del self.devices_by_scan_pid[process_id] - for i in range(col + 1, self.pm_noColumns): - oldWidget = self.pm_rows[rowPosition][i] - if oldWidget: - self.remove(oldWidget) - if oldWidget in self.pm_callbacks: - del self.pm_callbacks[oldWidget] - newWidget = widgets[i] - self.pm_rows[rowPosition][i] = newWidget - if newWidget: - self._createCallback(newWidget, rowPosition) - self.attach(newWidget, i, i+1, rowPosition, rowPosition + 1) - newWidget.show() - self.updatePreferences() - - - def on_entry_changed(self, widget, rowPosition): - self.updatePreferences() - - def on_rowAdded(self, rowPosition): + def get_all_displayed_processes(self): """ - Update preferences, as a row has been added + returns a list of the processes currently being displayed to the user """ - self.updatePreferences() - - # if this was the last row or 2nd to last row, and another has just been added, move vertical scrollbar down - if rowPosition in range(self.pm_noRows - 3, self.pm_noRows - 2): - adjustment = self.parentApp.rename_scrolledwindow.get_vadjustment() - adjustment.set_value(adjustment.upper) - + return self.map_process_to_row.keys() + - def on_rowDeleted(self, rowPosition): + def _set_process_map(self, process_id, iter): """ - Update preferences, as a row has been deleted + convert the temporary iter into a tree reference, which is + permanent """ - self.updatePreferences() - -class VideoRenameTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Video Rename preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.video_rename + path = self.liststore.get_path(iter) + treerowref = gtk.TreeRowReference(self.liststore, path) + self.map_process_to_row[process_id] = treerowref - def getPrefsFactory(self): - self.prefsFactory = rn.VideoRenamePreferences(self.prefList, self, - sequences = sequences) + def _get_process_map(self, process_id): + """ + return the tree iter for this process + """ - def updateParentAppPrefs(self): - self.parentApp.prefs.video_rename = self.prefList - - def updateExample(self): - self.parentApp.updateVideoRenameExample() - -class SubfolderTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Photo Download Subfolders preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.subfolder + if process_id in self.map_process_to_row: + treerowref = self.map_process_to_row[process_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + return iter + else: + return None - def getPrefsFactory(self): - self.prefsFactory = rn.SubfolderPreferences(self.prefList, self) + def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded): - def updateParentAppPrefs(self): - self.parentApp.prefs.subfolder = self.prefList + iter = self._get_process_map(scan_pid) + if iter: + if percent_complete: + self.liststore.set_value(iter, 3, percent_complete) + if progress_bar_text: + self.liststore.set_value(iter, 4, progress_bar_text) + if percent_complete or bytes_downloaded: + pass + #~ logger.info("Implement update overall progress") - def updateExample(self): - self.parentApp.updatePhotoDownloadFolderExample() + def button_clicked(self, widget, event): + """ + Look for left single click on eject button + """ + if event.button == 1: + x = int(event.x) + y = int(event.y) + path, column, cell_x, cell_y = self.get_path_at_pos(x, y) + if path is not None: + if column == self.get_column(0): + if cell_x >= column.get_width() - self.eject_pixbuf.get_width(): + iter = self.liststore.get_iter(path) + if self.liststore.get_value(iter, 5) is not None: + self.unmount(process_id = self.liststore.get_value(iter, 6)) + + def unmount(self, process_id): + device = self.devices_by_scan_pid[process_id] + if device.mount is not None: + logger.debug("Unmounting device with scan pid %s", process_id) + device.mount.unmount(self.unmount_callback) -class VideoSubfolderTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Video Download Subfolders preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.video_subfolder - def getPrefsFactory(self): - self.prefsFactory = rn.VideoSubfolderPreferences(self.prefList, self) - - def updateParentAppPrefs(self): - self.parentApp.prefs.video_subfolder = self.prefList + def unmount_callback(self, mount, result): + name = mount.get_name() - def updateExample(self): - self.parentApp.updateVideoDownloadFolderExample() - -class PreferencesDialog(gnomeglade.Component): - def __init__(self, parentApp): - gnomeglade.Component.__init__(self, - paths.share_dir(config.GLADE_FILE), - "preferencesdialog") - - self.widget.set_transient_for(parentApp.widget) - self.prefs = parentApp.prefs - - parentApp.preferencesDialogDisplayed = True + try: + mount.unmount_finish(result) + logger.debug("%s successfully unmounted" % name) + except gio.Error, inst: + logger.error("%s did not unmount: %s", name, inst) + + title = _("%(device)s did not unmount") % {'device': name} + message = '%s' % inst + + n = pynotify.Notification(title, message) + n.set_icon_from_pixbuf(self.parent_app.application_icon) + n.show() + + +def create_cairo_image_surface(pil_image, image_width, image_height): + imgd = pil_image.tostring("raw","BGRA", 0, 1) + data = array.array('B',imgd) + stride = image_width * 4 + image = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32, + image_width, image_height, stride) + return image + +class ThumbnailCellRenderer(gtk.CellRenderer): + __gproperties__ = { + "image": (gobject.TYPE_PYOBJECT, "Image", + "Image", gobject.PARAM_READWRITE), + + "filename": (gobject.TYPE_STRING, "Filename", + "Filename", '', gobject.PARAM_READWRITE), + + "status": (gtk.gdk.Pixbuf, "Status", + "Status", gobject.PARAM_READWRITE), + } + + def __init__(self, checkbutton_height): + gtk.CellRenderer.__init__(self) + self.image = None + + self.image_area_size = 100 + self.text_area_size = 30 + self.padding = 6 + self.checkbutton_height = checkbutton_height + self.icon_width = 20 - self.parentApp = parentApp + def do_set_property(self, pspec, value): + setattr(self, pspec.name, value) - self._setupTabSelector() + def do_get_property(self, pspec): + return getattr(self, pspec.name) - self._setupControlSpacing() - - if DOWNLOAD_VIDEO: - self.file_types = _("photos and videos") - else: - self.file_types = _("photos") - - # get example photo and video data - try: - w = workers.firstWorkerReadyToDownload() - mediaFile = w.firstImage() - self.sampleImageName = mediaFile.name - # assume the metadata is already read - self.sampleImage = mediaFile.metadata - except: - self.sampleImage = metadata.DummyMetaData() - self.sampleImageName = 'IMG_0524.CR2' - - try: - mediaFile = w.firstVideo() - self.sampleVideoName = mediaFile.name - self.sampleVideo = mediaFile.metadata - self.videoFallBackDate = mediaFile.modificationTime - except: - self.sampleVideo = videometadata.DummyMetaData() - self.sampleVideoName = 'MVI_1379.MOV' - self.videoFallBackDate = datetime.datetime.now() - + def do_render(self, window, widget, background_area, cell_area, expose_area, flags): - # setup tabs - self._setupPhotoDownloadFolderTab() - self._setupImageRenameTab() - self._setupVideoDownloadFolderTab() - self._setupVideoRenameTab() - self._setupRenameOptionsTab() - self._setupJobCodeTab() - self._setupDeviceTab() - self._setupBackupTab() - self._setupAutomationTab() - self._setupErrorTab() - - if not DOWNLOAD_VIDEO: - self.disableVideoControls() - - self.widget.realize() - - #set the width of the left column for selecting values - #note: this must be called after self.widget.realize(), or else the width calculation will fail - width_of_widest_sel_row = self.treeview.get_background_area(1, self.treeview_column)[2] - self.scrolled_window.set_size_request(width_of_widest_sel_row + 2, -1) - - #set the minimum width of the scolled window holding the photo rename table - if self.rename_scrolledwindow.get_vscrollbar(): - extra = self.rename_scrolledwindow.get_vscrollbar().allocation.width + 10 - else: - extra = 10 - self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) - - self.widget.show() - - def on_preferencesdialog_destroy(self, widget): - """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" - del self.sampleImage, self.rename_table.prefsFactory, self.subfolder_table.prefsFactory - - def _setupTabSelector(self): - self.notebook.set_show_tabs(0) - self.model = gtk.ListStore(type("")) - column = gtk.TreeViewColumn() - rentext = gtk.CellRendererText() - column.pack_start(rentext, expand=0) - column.set_attributes(rentext, text=0) - self.treeview_column = column - self.treeview.append_column(column) - self.treeview.props.model = self.model - for c in self.notebook.get_children(): - label = self.notebook.get_tab_label(c).get_text() - if not label.startswith("_"): - self.model.append( (label,) ) - - - # select the first value in the list store - self.treeview.set_cursor(0,column) - - def on_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.download_folder = widget.get_current_folder() - self.updatePhotoDownloadFolderExample() + cairo_context = window.cairo_create() - def on_video_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.video_download_folder = widget.get_current_folder() - self.updateVideoDownloadFolderExample() - - def on_backup_folder_filechooser_button_selection_changed(self, widget): - self.prefs.backup_location = widget.get_current_folder() - self.updateBackupExample() + x = cell_area.x + y = cell_area.y + self.checkbutton_height - 8 + w = cell_area.width + h = cell_area.height - def on_device_location_filechooser_button_selection_changed(self, widget): - self.prefs.device_location = widget.get_current_folder() + #constrain operations to cell area, allowing for a 1 pixel border + #either side + #~ cairo_context.rectangle(x-1, y-1, w+2, h+2) + #~ cairo_context.clip() - def _setupControlSpacing(self): - """ - set spacing of some but not all controls - """ + #fill in the background with dark grey + #this ensures that a selected cell's fill does not make + #the text impossible to read + #~ cairo_context.rectangle(x, y, w, h) + #~ cairo_context.set_source_rgb(0.267, 0.267, 0.267) + #~ cairo_context.fill() - self._setupTableSpacing(self.download_folder_table) - self._setupTableSpacing(self.video_download_folder_table) - self.download_folder_table.set_row_spacing(2, - hd.VERTICAL_CONTROL_SPACE) - self.video_download_folder_table.set_row_spacing(2, - hd.VERTICAL_CONTROL_SPACE) - self._setupTableSpacing(self.rename_example_table) - self._setupTableSpacing(self.video_rename_example_table) - self.devices_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) - - self._setupTableSpacing(self.backup_table) - self.backup_table.set_col_spacing(1, hd.NESTED_CONTROLS_SPACE) - self.backup_table.set_col_spacing(2, hd.CONTROL_LABEL_SPACE) - self._setupTableSpacing(self.compatibility_table) - self.compatibility_table.set_row_spacing(0, - hd.VERTICAL_CONTROL_LABEL_SPACE) - self._setupTableSpacing(self.error_table) - - - def _setupTableSpacing(self, table): - table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) - table.set_col_spacing(1, hd.CONTROL_LABEL_SPACE) - - def _setupSubfolderTable(self): - self.subfolder_table = SubfolderTable(self, None) - self.subfolder_vbox.pack_start(self.subfolder_table) - self.subfolder_table.show_all() - - def _setupVideoSubfolderTable(self): - self.video_subfolder_table = VideoSubfolderTable(self, None) - self.video_subfolder_vbox.pack_start(self.video_subfolder_table) - self.video_subfolder_table.show_all() - - def _setupPhotoDownloadFolderTab(self): - self.download_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder to download photos to")) - self.download_folder_filechooser_button.set_current_folder( - self.prefs.download_folder) - self.download_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.download_folder_filechooser_button.connect("selection-changed", - self.on_download_folder_filechooser_button_selection_changed) - - self.download_folder_table.attach( - self.download_folder_filechooser_button, - 2, 3, 2, 3, yoptions = gtk.SHRINK) - self.download_folder_filechooser_button.show() - - self._setupSubfolderTable() - self.updatePhotoDownloadFolderExample() - - def _setupVideoDownloadFolderTab(self): - self.video_download_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder to download videos to")) - self.video_download_folder_filechooser_button.set_current_folder( - self.prefs.video_download_folder) - self.video_download_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.video_download_folder_filechooser_button.connect("selection-changed", - self.on_video_download_folder_filechooser_button_selection_changed) - - self.video_download_folder_table.attach( - self.video_download_folder_filechooser_button, - 2, 3, 2, 3, yoptions = gtk.SHRINK) - self.video_download_folder_filechooser_button.show() - self._setupVideoSubfolderTable() - self.updateVideoDownloadFolderExample() - - def _setupImageRenameTab(self): - - self.rename_table = ImageRenameTable(self, self.rename_scrolledwindow) - self.rename_table_vbox.pack_start(self.rename_table) - self.rename_table.show_all() - self.original_name_label.set_markup("<i>%s</i>" % self.sampleImageName) - self.updateImageRenameExample() + #image width and height + image_w = self.image.size[0] + image_h = self.image.size[1] - def _setupVideoRenameTab(self): + #center the image horizontally + #bottom align vertically + #top left and right corners for the image: + image_x = x + ((w - image_w) / 2) + image_y = y + self.image_area_size - image_h - self.video_rename_table = VideoRenameTable(self, self.video_rename_scrolledwindow) - self.video_rename_table_vbox.pack_start(self.video_rename_table) - self.video_rename_table.show_all() - self.video_original_name_label.set_markup("<i>%s</i>" % self.sampleVideoName) - self.updateVideoRenameExample() - - def _setupRenameOptionsTab(self): - - # sequence numbers - self.downloads_today_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 0)) - self.stored_number_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 1)) - self.downloads_today_entry.connect('changed', self.on_downloads_today_entry_changed) - self.stored_number_entry.connect('changed', self.on_stored_number_entry_changed) - v = self.prefs.getAndMaybeResetDownloadsToday() - self.downloads_today_entry.set_text(str(v)) - # make the displayed value of stored sequence no 1 more than actual value - # so as not to confuse the user - self.stored_number_entry.set_text(str(self.prefs.stored_sequence_no+1)) - self.sequence_vbox.pack_start(self.downloads_today_entry, expand=True, fill=True) - self.sequence_vbox.pack_start(self.stored_number_entry, expand=False) - self.downloads_today_entry.show() - self.stored_number_entry.show() - hour, minute = self.prefs.getDayStart() - self.hour_spinbutton.set_value(float(hour)) - self.minute_spinbutton.set_value(float(minute)) - - self.synchronize_raw_jpg_checkbutton.set_active( - self.prefs.synchronize_raw_jpg) - - #compatibility - self.strip_characters_checkbutton.set_active( - self.prefs.strip_characters) - - def _setupJobCodeTab(self): - self.job_code_liststore = gtk.ListStore(str) - column = gtk.TreeViewColumn() - rentext = gtk.CellRendererText() - rentext.connect('edited', self.on_job_code_edited) - rentext .set_property('editable', True) - - column.pack_start(rentext, expand=0) - column.set_attributes(rentext, text=0) - self.job_code_treeview_column = column - self.job_code_treeview.append_column(column) - self.job_code_treeview.props.model = self.job_code_liststore - for code in self.prefs.job_codes: - self.job_code_liststore.append((code, )) - - # set multiple selections - self.job_code_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - - self.remove_all_job_code_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CLEAR, - gtk.ICON_SIZE_BUTTON)) - def _setupDeviceTab(self): - - self.device_location_filechooser_button = gtk.FileChooserButton( - _("Select a folder containing %(file_types)s") % {'file_types':self.file_types}) - self.device_location_filechooser_button.set_current_folder( - self.prefs.device_location) - self.device_location_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - - self.device_location_filechooser_button.connect("selection-changed", - self.on_device_location_filechooser_button_selection_changed) - - self.devices2_table.attach(self.device_location_filechooser_button, - 1, 2, 1, 2, xoptions = gtk.EXPAND|gtk.FILL, yoptions = gtk.SHRINK) - self.device_location_filechooser_button.show() - self.autodetect_device_checkbutton.set_active( - self.prefs.device_autodetection) - self.autodetect_psd_checkbutton.set_active( - self.prefs.device_autodetection_psd) - - self.updateDeviceControls() - - - def _setupBackupTab(self): - self.backup_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder in which to backup %(file_types)s") % {'file_types':self.file_types}) - self.backup_folder_filechooser_button.set_current_folder( - self.prefs.backup_location) - self.backup_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.backup_folder_filechooser_button.connect("selection-changed", - self.on_backup_folder_filechooser_button_selection_changed) - self.backup_table.attach(self.backup_folder_filechooser_button, - 3, 4, 8, 9, yoptions = gtk.SHRINK) - self.backup_folder_filechooser_button.show() - self.backup_identifier_entry.set_text(self.prefs.backup_identifier) - self.video_backup_identifier_entry.set_text(self.prefs.video_backup_identifier) - - #setup controls for manipulating sensitivity - self._backupControls0 = [self.auto_detect_backup_checkbutton] - self._backupControls1 = [self.backup_identifier_explanation_label, - self.backup_identifier_label, - self.backup_identifier_entry, - self.example_backup_path_label, - self.backup_example_label,] - self._backupControls2 = [self.backup_location_label, - self.backup_folder_filechooser_button, - self.backup_location_explanation_label] - self._backupControls = self._backupControls0 + self._backupControls1 + \ - self._backupControls2 - - self._backupVideoControls = [self.video_backup_identifier_label, - self.video_backup_identifier_entry] - - #assign values to checkbuttons only when other controls - #have been setup, because their toggle signal is activated - #when a value is assigned - - self.backup_checkbutton.set_active(self.prefs.backup_images) - self.auto_detect_backup_checkbutton.set_active( - self.prefs.backup_device_autodetection) - self.updateBackupControls() - self.updateBackupExample() - - def _setupAutomationTab(self): - self.auto_startup_checkbutton.set_active( - self.prefs.auto_download_at_startup) - self.auto_insertion_checkbutton.set_active( - self.prefs.auto_download_upon_device_insertion) - self.auto_unmount_checkbutton.set_active( - self.prefs.auto_unmount) - self.auto_exit_checkbutton.set_active( - self.prefs.auto_exit) - self.auto_delete_checkbutton.set_active( - self.prefs.auto_delete) - - - def _setupErrorTab(self): - if self.prefs.download_conflict_resolution == config.SKIP_DOWNLOAD: - self.skip_download_radiobutton.set_active(True) - else: - self.add_identifier_radiobutton.set_active(True) - - if self.prefs.backup_duplicate_overwrite: - self.backup_duplicate_overwrite_radiobutton.set_active(True) - else: - self.backup_duplicate_skip_radiobutton.set_active(True) + #convert PIL image to format suitable for cairo + image = create_cairo_image_surface(self.image, image_w, image_h) - - def updateExampleFileName(self, display_table, rename_table, sample, sampleName, example_label, fallback_date = None): - problem = pn.Problem() - if hasattr(self, display_table): - rename_table.updateExampleJobCode() - rename_table.prefsFactory.initializeProblem(problem) - name = rename_table.prefsFactory.generateNameUsingPreferences( - sample, sampleName, - self.prefs.strip_characters, sequencesPreliminary=False, fallback_date=fallback_date) - else: - name = '' - - # since this is markup, escape it - text = "<i>%s</i>" % common.escape(name) + # draw a light grey border of 1px around the image + cairo_context.set_source_rgb(0.66, 0.66, 0.66) #light grey, #a9a9a9 + cairo_context.set_line_width(1) + cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1) + cairo_context.stroke() - if problem.has_problem(): - text += "\n" - # Translators: please do not modify or leave out html formatting tags like <i> and <b>. These are used to format the text the users sees - text += _("<i><b>Warning:</b> There is insufficient metadata to fully generate the name. Please use other renaming options.</i>") - - example_label.set_markup(text) - - def updateImageRenameExample(self): - """ - Displays example image name to the user - """ - self.updateExampleFileName('rename_table', self.rename_table, self.sampleImage, self.sampleImageName, self.new_name_label) - + # draw a thin border around each cell + #~ cairo_context.set_source_rgb(0.33,0.33,0.33) + #~ cairo_context.rectangle(x, y, w, h) + #~ cairo_context.stroke() - def updateVideoRenameExample(self): - """ - Displays example video name to the user - """ - self.updateExampleFileName('video_rename_table', self.video_rename_table, self.sampleVideo, self.sampleVideoName, self.video_new_name_label, self.videoFallBackDate) - - def updateDownloadFolderExample(self, display_table, subfolder_table, download_folder, sample, sampleName, example_download_path_label, subfolder_warning_label, fallback_date = None): - """ - Displays example subfolder name(s) to the user - """ + #place the image + cairo_context.set_source_surface(image, image_x, image_y) + cairo_context.paint() - problem = pn.Problem() - if hasattr(self, display_table): - subfolder_table.updateExampleJobCode() - subfolder_table.prefsFactory.initializeProblem(problem) - path = subfolder_table.prefsFactory.generateNameUsingPreferences( - sample, sampleName, - self.prefs.strip_characters, fallback_date = fallback_date) - else: - path = '' + #text + context = pangocairo.CairoContext(cairo_context) - text = os.path.join(download_folder, path) - # since this is markup, escape it - path = common.escape(text) - if problem.has_problem(): - warning = _("<i><b>Warning:</b> There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options.</i>" ) - else: - warning = "" - # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see - example_download_path_label.set_markup(_("<i>Example: %s</i>") % text) - subfolder_warning_label.set_markup(warning) - - def updatePhotoDownloadFolderExample(self): - if hasattr(self, 'subfolder_table'): - self.updateDownloadFolderExample('subfolder_table', self.subfolder_table, self.prefs.download_folder, self.sampleImage, self.sampleImageName, self.example_photo_download_path_label, self.photo_subfolder_warning_label) - - def updateVideoDownloadFolderExample(self): - if hasattr(self, 'video_subfolder_table'): - self.updateDownloadFolderExample('video_subfolder_table', self.video_subfolder_table, self.prefs.video_download_folder, self.sampleVideo, self.sampleVideoName, self.example_video_download_path_label, self.video_subfolder_warning_label, self.videoFallBackDate) - - def on_hour_spinbutton_value_changed(self, spinbutton): - hour = spinbutton.get_value_as_int() - minute = self.minute_spinbutton.get_value_as_int() - self.prefs.setDayStart(hour, minute) - self.on_downloads_today_entry_changed(self.downloads_today_entry) - - def on_minute_spinbutton_value_changed(self, spinbutton): - hour = self.hour_spinbutton.get_value_as_int() - minute = spinbutton.get_value_as_int() - self.prefs.setDayStart(hour, minute) - self.on_downloads_today_entry_changed(self.downloads_today_entry) - - def on_downloads_today_entry_changed(self, entry): - # do not update value if a download is occurring - it will mess it up! - if workers.noDownloadingWorkers() <> 0: - cmd_line(_("Downloads today value not updated, as a download is currently occurring")) - else: - v = entry.get_text() - try: - v = int(v) - except: - v = 0 - if v < 0: - v = 0 - self.prefs.resetDownloadsToday(v) - sequences.setDownloadsToday(v) - self.updateImageRenameExample() - - def on_stored_number_entry_changed(self, entry): - # do not update value if a download is occurring - it will mess it up! - if workers.noDownloadingWorkers() <> 0: - cmd_line(_("Stored number value not updated, as a download is currently occurring")) - else: - v = entry.get_text() - try: - # the displayed value of stored sequence no 1 more than actual value - # so as not to confuse the user - v = int(v) - 1 - except: - v = 0 - if v < 0: - v = 0 - self.prefs.stored_sequence_no = v - sequences.setStoredSequenceNo(v) - self.updateImageRenameExample() - - def _updateSubfolderPrefOnError(self, newPrefList): - self.prefs.subfolder = newPrefList - - def _updateVideoSubfolderPrefOnError(self, newPrefList): - self.prefs.video_subfolder = newPrefList - - - def checkSubfolderValuesValidOnExit(self, usersPrefList, updatePrefFunction, filetype, defaultPrefList): - """ - Checks that the user has not entered in any inappropriate values + text_y = y + self.image_area_size + 10 + text_w = w - self.icon_width + text_x = x + self.icon_width + #~ context.rectangle(text_x, text_y, text_w, 15) + #~ context.clip() - If they have, filters out bad values and warns the user - """ - filtered, prefList = rn.filterSubfolderPreferences(usersPrefList) - if filtered: - cmd_line(_("The %(filetype)s subfolder preferences had some unnecessary values removed.") % {'filetype': filetype}) - if prefList: - updatePrefFunction(prefList) - else: - #Preferences list is now empty - msg = _("The %(filetype)s subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") % {'filetype': filetype} - sys.stderr.write(msg + "\n") - misc.run_dialog(PROGRAM_NAME, msg) - updatePrefFunction(self.prefs.get_default(defaultPrefList)) - - def on_response(self, dialog, arg): - if arg == gtk.RESPONSE_HELP: - webbrowser.open("http://www.damonlynch.net/rapid/documentation") - else: - # arg==gtk.RESPONSE_CLOSE, or the user hit the 'x' to close the window - self.prefs.backup_identifier = self.backup_identifier_entry.get_property("text") - self.prefs.video_backup_identifier = self.video_backup_identifier_entry.get_property("text") - - #check subfolder preferences for bad values - self.checkSubfolderValuesValidOnExit(self.prefs.subfolder, self._updateSubfolderPrefOnError, _("photo"), "subfolder") - self.checkSubfolderValuesValidOnExit(self.prefs.video_subfolder, self._updateVideoSubfolderPrefOnError, _("video"), "video_subfolder") - - self.widget.destroy() - self.parentApp.preferencesDialogDisplayed = False - self.parentApp.postPreferenceChange() - + layout = context.create_layout() - - - def on_add_job_code_button_clicked(self, button): - j = JobCodeDialog(self.widget, self.prefs.job_codes, None, self.add_job_code, False, True, True) - - - def add_job_code(self, dialog, userChoseCode, job_code, autoStart, downloadSelected): - dialog.destroy() - if userChoseCode: - if job_code and job_code not in self.prefs.job_codes: - self.job_code_liststore.prepend((job_code, )) - self.update_job_codes() - selection = self.job_code_treeview.get_selection() - selection.unselect_all() - selection.select_path((0, )) - #scroll to the top - adjustment = self.job_code_scrolledwindow.get_vadjustment() - adjustment.set_value(adjustment.lower) - - def on_remove_job_code_button_clicked(self, button): - """ remove selected job codes (can be multiple selection)""" - selection = self.job_code_treeview.get_selection() - model, selected = selection.get_selected_rows() - iters = [model.get_iter(path) for path in selected] - # only delete if a jobe code is selected - if iters: - no = len(iters) - path = None - for i in range(0, no): - iter = iters[i] - if i == no - 1: - path = model.get_path(iter) - model.remove(iter) - - # now that we removed the selection, play nice with - # the user and select the next item - selection.select_path(path) - - # if there was no selection that meant the user - # removed the last entry, so we try to select the - # last item - if not selection.path_is_selected(path): - row = path[0]-1 - # test case for empty lists - if row >= 0: - selection.select_path((row,)) - - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_remove_all_job_code_button_clicked(self, button): - j = RemoveAllJobCodeDialog(self.widget, self.remove_all_job_code) - - def remove_all_job_code(self, dialog, userSelected): - dialog.destroy() - if userSelected: - self.job_code_liststore.clear() - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_job_code_edited(self, widget, path, new_text): - iter = self.job_code_liststore.get_iter(path) - self.job_code_liststore.set_value(iter, 0, new_text) - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def update_job_codes(self): - """ update preferences with list of job codes""" - job_codes = [] - for row in self.job_code_liststore: - job_codes.append(row[0]) - self.prefs.job_codes = job_codes - - def on_auto_startup_checkbutton_toggled(self, checkbutton): - self.prefs.auto_download_at_startup = checkbutton.get_active() - - def on_auto_insertion_checkbutton_toggled(self, checkbutton): - self.prefs.auto_download_upon_device_insertion = checkbutton.get_active() - - def on_auto_unmount_checkbutton_toggled(self, checkbutton): - self.prefs.auto_unmount = checkbutton.get_active() - - - def on_auto_delete_checkbutton_toggled(self, checkbutton): - self.prefs.auto_delete = checkbutton.get_active() - - def on_auto_exit_checkbutton_toggled(self, checkbutton): - self.prefs.auto_exit = checkbutton.get_active() - - def on_autodetect_device_checkbutton_toggled(self, checkbutton): - self.prefs.device_autodetection = checkbutton.get_active() - self.updateDeviceControls() - - def on_autodetect_psd_checkbutton_toggled(self, checkbutton): - self.prefs.device_autodetection_psd = checkbutton.get_active() - - def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): - self.prefs.backup_duplicate_overwrite = widget.get_active() - - def on_backup_duplicate_skip_radiobutton_toggled(self, widget): - self.prefs.backup_duplicate_overwrite = not widget.get_active() - - def on_treeview_cursor_changed(self, tree): - path, column = tree.get_cursor() - self.notebook.set_current_page(path[0]) - - def on_synchronize_raw_jpg_checkbutton_toggled(self, check_button): - self.prefs.synchronize_raw_jpg = check_button.get_active() - - def on_strip_characters_checkbutton_toggled(self, check_button): - self.prefs.strip_characters = check_button.get_active() - self.updateImageRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_add_identifier_radiobutton_toggled(self, widget): - if widget.get_active(): - self.prefs.download_conflict_resolution = config.ADD_UNIQUE_IDENTIFIER - else: - self.prefs.download_conflict_resolution = config.SKIP_DOWNLOAD - - - def updateDeviceControls(self): - """ - Sets sensitivity of image device controls - """ - controls = [self.device_location_explanation_label, - self.device_location_label, - self.device_location_filechooser_button] - - if self.prefs.device_autodetection: - for c in controls: - c.set_sensitive(False) - self.autodetect_psd_checkbutton.set_sensitive(True) - self.autodetect_image_devices_label.set_sensitive(True) - else: - for c in controls: - c.set_sensitive(True) - self.autodetect_psd_checkbutton.set_sensitive(False) - self.autodetect_image_devices_label.set_sensitive(False) - - def updateBackupControls(self): - """ - Sets sensitivity of backup related widgets - """ + width = text_w * pango.SCALE + layout.set_width(width) - if not self.backup_checkbutton.get_active(): - for c in self._backupControls + self._backupVideoControls: - c.set_sensitive(False) + layout.set_alignment(pango.ALIGN_CENTER) + layout.set_ellipsize(pango.ELLIPSIZE_END) + + #font color and size + fg_color = pango.AttrForeground(65535, 65535, 65535, 0, -1) + font_size = pango.AttrSize(8192, 0, -1) # 8 * 1024 = 8192 + font_family = pango.AttrFamily('sans', 0, -1) + attr = pango.AttrList() + attr.insert(fg_color) + attr.insert(font_size) + attr.insert(font_family) + layout.set_attributes(attr) - else: - for c in self._backupControls0: - c.set_sensitive(True) - self.updateBackupControlsAuto() - - def updateBackupControlsAuto(self): - """ - Sets sensitivity of subset of backup related widgets - """ - - if self.auto_detect_backup_checkbutton.get_active(): - for c in self._backupControls1: - c.set_sensitive(True) - for c in self._backupControls2: - c.set_sensitive(False) - for c in self._backupVideoControls: - c.set_sensitive(False) - if DOWNLOAD_VIDEO: - for c in self._backupVideoControls: - c.set_sensitive(True) - else: - for c in self._backupControls1: - c.set_sensitive(False) - for c in self._backupControls2: - c.set_sensitive(True) - if DOWNLOAD_VIDEO: - for c in self._backupVideoControls: - c.set_sensitive(False) - - def disableVideoControls(self): - """ - Disables video preferences if video downloading is disabled - (probably because the appropriate libraries to enable - video metadata extraction are not installed) - """ - controls = [self.example_video_filename_label, - self.original_video_filename_label, - self.new_video_filename_label, - self.video_new_name_label, - self.video_original_name_label, - self.video_rename_scrolledwindow, - self.video_folders_hbox, - self.video_backup_identifier_label, - self.video_backup_identifier_entry - ] - for c in controls: - c.set_sensitive(False) - - self.videos_cannot_be_downloaded_label.show() - self.folder_videos_cannot_be_downloaded_label.show() - self.folder_videos_cannot_be_downloaded_hbox.show() - - def on_auto_detect_backup_checkbutton_toggled(self, widget): - self.prefs.backup_device_autodetection = widget.get_active() - self.updateBackupControlsAuto() - - def on_backup_checkbutton_toggled(self, widget): - self.prefs.backup_images = self.backup_checkbutton.get_active() - self.updateBackupControls() + layout.set_text(self.filename) - def on_backup_identifier_entry_changed(self, widget): - self.updateBackupExample() - - def on_video_backup_identifier_entry_changed(self, widget): - self.updateBackupExample() - - def on_backup_scan_folder_on_entry_changed(self, widget): - self.updateBackupExample() - - def updateBackupExample(self): - # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. - drive1 = os.path.join(config.MEDIA_LOCATION, _("externaldrive1")) - # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. - drive2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) - - path = os.path.join(drive1, self.backup_identifier_entry.get_text()) - path2 = os.path.join(drive2, self.backup_identifier_entry.get_text()) - path3 = os.path.join(drive2, self.video_backup_identifier_entry.get_text()) - path = common.escape(path) - path2 = common.escape(path2) - path3 = common.escape(path3) - if DOWNLOAD_VIDEO: - example = "<i>%s</i>\n<i>%s</i>\n<i>%s</i>" % (path, path2, path3) - else: - example = "<i>%s</i>\n<i>%s</i>" % (path, path2) - self.example_backup_path_label.set_markup(example) + context.move_to(text_x, text_y) + context.show_layout(layout) + #status + cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10) + cairo_context.paint() -def file_types_by_number(noImages, noVideos): - """ - returns a string to be displayed to the user that can be used - to show if a value refers to photos or videos or both, or just one - of each - """ - if (noVideos > 0) and (noImages > 0): - v = _('photos and videos') - elif (noVideos == 0) and (noImages == 0): - v = _('photos or videos') - elif noVideos > 0: - if noVideos > 1: - v = _('videos') - else: - v = _('video') - else: - if noImages > 1: - v = _('photos') - else: - v = _('photo') - return v - -def date_time_human_readable(date, with_line_break=True): - if with_line_break: - return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} - else: - return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} + def do_get_size(self, widget, cell_area): + return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4) -def time_subseconds_human_readable(date, subseconds): - return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - -def date_time_subseconds_human_readable(date, subseconds): - return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'date':date.strftime("%x"), - 'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - -def generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, - renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - strip_characters, fallback_date): - - subfolderPrefsFactory.initializeProblem(problem) - mediaFile.sampleSubfolder = subfolderPrefsFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, - strip_characters, - fallback_date = fallback_date) - - mediaFile.samplePath = os.path.join(mediaFile.downloadFolder, mediaFile.sampleSubfolder) - - renamePrefsFactory.initializeProblem(problem) - mediaFile.sampleName = renamePrefsFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, strip_characters, - sequencesPreliminary=False, - fallback_date = fallback_date) - - if not (mediaFile.sampleName or nameUsesJobCode) or not (mediaFile.sampleSubfolder or subfolderUsesJobCode): - if not (mediaFile.sampleName or nameUsesJobCode) and not (mediaFile.sampleSubfolder or subfolderUsesJobCode): - area = _("subfolder and filename") - elif not (mediaFile.sampleName or nameUsesJobCode): - area = _("filename") - else: - area = _("subfolder") - problem.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': mediaFile.displayNameCap, 'area': area}) - problem.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - mediaFile.problem = problem - mediaFile.status = STATUS_CANNOT_DOWNLOAD - elif problem.has_problem(): - mediaFile.problem = problem - mediaFile.status = STATUS_WARNING - else: - mediaFile.status = STATUS_NOT_DOWNLOADED -def getGenericPhotoImage(): - return gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo.png')) - -def getGenericVideoImage(): - return gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video.png')) +gobject.type_register(ThumbnailCellRenderer) + -class NeedAJobCode(): - """ - Convenience class to check whether a job code is missing for a given - file type (photo or video) - """ - def __init__(self, prefs): - self.imageRenameUsesJobCode = rn.usesJobCode(prefs.image_rename) - self.imageSubfolderUsesJobCode = rn.usesJobCode(prefs.subfolder) - self.videoRenameUsesJobCode = rn.usesJobCode(prefs.video_rename) - self.videoSubfolderUsesJobCode = rn.usesJobCode(prefs.video_subfolder) +class ThumbnailDisplay(gtk.IconView): + def __init__(self, parent_app): + gtk.IconView.__init__(self) + self.set_spacing(0) + self.set_row_spacing(5) + self.set_margin(25) - def needAJobCode(self, job_code, is_image): - if is_image: - return not job_code and (self.imageRenameUsesJobCode or self.imageSubfolderUsesJobCode) - else: - return not job_code and (self.videoRenameUsesJobCode or self.videoSubfolderUsesJobCode) + self.rapid_app = parent_app - -class CopyPhotos(Thread): - """Copies photos from source to destination, backing up if needed""" - def __init__(self, thread_id, parentApp, fileRenameLock, fileSequenceLock, - statsLock, downloadedFilesLock, - downloadStats, autoStart = False, cardMedia = None): - self.parentApp = parentApp - self.thread_id = thread_id - self.ctrl = True - self.running = False - self.manuallyDisabled = False - # enable the capacity to block oneself with a lock - # the lock will be first set when the thread begins - # it will then be locked when the thread needs to be paused - # releasing it will cause the code to restart from where it - # left off - self.lock = Lock() + self.batch_size = 10 - self.fileRenameLock = fileRenameLock - self.fileSequenceLock = fileSequenceLock - self.statsLock = statsLock - self.downloadedFilesLock = downloadedFilesLock + self.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size) + self.preview_manager = PreviewManager(self.preview_results) - self.downloadStats = downloadStats + self.treerow_index = {} + self.process_index = {} - self.hasStarted = False - self.doNotStart = False - self.waitingForJobCode = False + self.rpd_files = {} - self.autoStart = autoStart - self.cardMedia = cardMedia + self.total_thumbs_to_generate = 0 + self.thumbnails_generated = 0 - self.initializeDisplay(thread_id, self.cardMedia) - - self.scanComplete = self.downloadStarted = self.downloadComplete = False + self.thumbnails = {} + self.previews = {} + self.previews_being_fetched = set() - # Need to account for situations where the user adjusts their preferences when the program is scanning - # Here the sample filenames and paths will be out of date, and they will need to be updated - # This flag indicates whether that is the case or not - self.scanResultsStale = False # name and subfolder - self.scanResultsStaleDownloadFolder = False #download folder only + self.stock_photo_thumbnails = tn.PhotoIcons() + self.stock_video_thumbnails = tn.VideoIcons() - self.noErrors = self.noWarnings = 0 - self.videoTempWorkingDir = self.photoTempWorkingDir = '' + self.SELECTED_COL = 1 + self.UNIQUE_ID_COL = 2 + self.TIMESTAMP_COL = 4 + self.FILETYPE_COL = 5 + self.CHECKBUTTON_VISIBLE_COL = 6 + self.DOWNLOAD_STATUS_COL = 7 + self.STATUS_ICON_COL = 8 - if DOWNLOAD_VIDEO: - self.types_searched_for = _('photos or videos') - else: - self.types_searched_for = _('photos') + self.liststore = gtk.ListStore( + gobject.TYPE_PYOBJECT, # 0 PIL thumbnail + gobject.TYPE_BOOLEAN, # 1 selected or not + str, # 2 unique id + str, # 3 file name + int, # 4 timestamp for sorting, converted float + int, # 5 file type i.e. photo or video + gobject.TYPE_BOOLEAN, # 6 visibility of checkbutton + int, # 7 status of download + gtk.gdk.Pixbuf, # 8 status icon + ) + + self.clear() + self.set_model(self.liststore) - Thread.__init__(self) + checkbutton = gtk.CellRendererToggle() + checkbutton.set_radio(False) + checkbutton.props.activatable = True + checkbutton.props.xalign = 0.0 + checkbutton.connect('toggled', self.on_checkbutton_toggled) + self.pack_end(checkbutton, expand=False) - def initializeDisplay(self, thread_id, cardMedia = None): + self.add_attribute(checkbutton, "active", 1) + self.add_attribute(checkbutton, "visible", 6) + + checkbutton_size = checkbutton.get_size(self, None) + checkbutton_height = checkbutton_size[3] + checkbutton_width = checkbutton_size[2] + + image = ThumbnailCellRenderer(checkbutton_height) + self.pack_start(image, expand=True) + self.add_attribute(image, "image", 0) + self.add_attribute(image, "filename", 3) + self.add_attribute(image, "status", 8) - if self.cardMedia: - media_collection_treeview.addCard(thread_id, self.cardMedia.prettyName(), - '', progress=0.0, - # This refers to when a device like a hard drive is having its contents scanned, - # looking for photos or videos. It is visible initially in the progress bar for each device - # (which normally holds "x photos and videos"). - # It maybe displayed only briefly if the contents of the device being scanned is small. - progressBarText=_('scanning...')) - def firstImage(self): - """ - returns class mediaFile of the first photo - """ - mediaFile = self.cardMedia.firstImage() - return mediaFile - - def firstVideo(self): - """ - returns class mediaFile of the first video - """ - mediaFile = self.cardMedia.firstVideo() - return mediaFile - - def handlePreferencesError(self, e, prefsFactory): - sys.stderr.write(_("Sorry,these preferences contain an error:\n")) - sys.stderr.write(prefsFactory.formatPreferencesForPrettyPrint() + "\n") - msg = str(e) - sys.stderr.write(msg + "\n") - def initializeFromPrefs(self, notifyOnError): - """ - Setup thread so that user preferences are handled - """ + #set the background color to a darkish grey + self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444')) - def checkPrefs(prefsFactory): - try: - prefsFactory.checkPrefsForValidity() - except (rn.PrefValueInvalidError, rn.PrefLengthError, - rn.PrefValueKeyComboError, rn.PrefKeyError), e: - if notifyOnError: - self.handlePreferencesError(e, prefsFactory) - raise rn.PrefError - - self.prefs = self.parentApp.prefs + self.show_all() - #Image and Video filename preferences - sample_download_start_time = datetime.datetime.now() + self._setup_icons() + - self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.prefs.image_rename, self, - self.fileSequenceLock, sequences) - self.imageRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.imageRenamePrefsFactory) - - self.videoRenamePrefsFactory = rn.VideoRenamePreferences(self.prefs.video_rename, self, - self.fileSequenceLock, sequences) - self.videoRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.videoRenamePrefsFactory) - #Image and Video subfolder preferences - - self.subfolderPrefsFactory = rn.SubfolderPreferences(self.prefs.subfolder, self) - self.subfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.subfolderPrefsFactory) - - self.videoSubfolderPrefsFactory = rn.VideoSubfolderPreferences(self.prefs.video_subfolder, self) - self.videoSubfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.videoSubfolderPrefsFactory) + self.connect('item-activated', self.on_item_activated) - # copy this variable, as it is used heavily in the loop - # and it is perhaps relatively expensive to read - self.stripCharacters = self.prefs.strip_characters + def _setup_icons(self): + # icons to be displayed in status column - def run(self): + size = 16 + # standard icons + failed = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) + self.download_failed_icon = failed.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + error = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) + self.error_icon = error.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + warning = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) + self.warning_icon = warning.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + + # Rapid Photo Downloader specific icons + self.downloaded_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded.svg'), + size, size) + self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-download-pending.png'), + size, size) + self.downloaded_with_warning_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-warning.svg'), + size, size) + self.downloaded_with_error_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-error.svg'), + size, size) + + # make the not yet downloaded icon a transparent square + self.not_downloaded_icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 16, 16) + self.not_downloaded_icon.fill(0xffffffff) + self.not_downloaded_icon = self.not_downloaded_icon.add_alpha(True, chr(255), chr(255), chr(255)) + + def get_status_icon(self, status): """ - Copy photos from device to local drive, and if requested, backup - - 1. Should the image be downloaded? - 1.a generate file name - 1.a.1 generate sequence numbers if needed - 1.a.2 FIFO queue sequence numbers to indicate that they could - potentially be used in a filename - 1.b check to see if a file exists with the same name in the place it will - be downloaded to - 1.c if it exisits, and unique identifiers are not being used: - 1.b.1 if using sequence numbers or letters, then potentially any of the - sequence numbers in the queue could be used to make the filename - 1.b.1.a generate and check each filename using sequence numbers in the queue - 1.b.1.b if one of these filenames is unique, then image needs to be downloaded - 1.b.2 do not do not download - - - 2. Download the image - 2.a copy it to temporary folder (this takes time) - 2.b is the file name still unique? Perhaps a new file was created with this name in the meantime - (either by another thread or another program) - 2.b.1 don't allow any other thread to rename a file - 2.b.2 check file name - 2.b.3 adding suffix if it is not unique, being careful not to overwrite any existing file with a suffix - 2.b.4 rename it to the "real" name, effectively performing a mv - 2.b.5 allow other threads to rename files - - 3. Backup the image, using the same filename as was used when it was downloaded - 3.a does a file with the same name already exist on the backup medium? - 3.b if so, user preferences determine whether it should be overwritten or not + Returns the correct icon, based on the status """ - - def checkDownloadPath(path): - """ - Checks to see if download folder exists. - - Creates it if it does not exist. - - Returns False if the path could not be created. - """ - - try: - if not os.path.isdir(path): - os.makedirs(path) - return True - - except: - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - msg = _("The following download path could not be created:\n") - msg += _("%(path)s: ") % {'path': path} - logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) - cmd_line(_("Download cannot proceed")) - cmd_line(msg) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - return False - - def getPrefs(notifyOnError): - try: - self.initializeFromPrefs(notifyOnError) - return True - except rn.PrefError: - if notifyOnError: - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - msg = _("There is an error in the program preferences.") - msg += _("\nPlease check preferences, restart the program, and try again.") - logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) - cmd_line(_("Download cannot proceed")) - cmd_line(msg) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - return False + if status == STATUS_WARNING: + status_icon = self.warning_icon + elif status == STATUS_CANNOT_DOWNLOAD: + status_icon = self.error_icon + elif status == STATUS_DOWNLOADED: + status_icon = self.downloaded_icon + elif status == STATUS_NOT_DOWNLOADED: + status_icon = self.not_downloaded_icon + elif status in [STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]: + status_icon = self.downloaded_with_warning_icon + elif status in [STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOAD_AND_BACKUP_FAILED]: + status_icon = self.downloaded_with_error_icon + elif status == STATUS_DOWNLOAD_PENDING: + status_icon = self.download_pending_icon + else: + logger.critical("FIXME: unknown status: %s", status) + status_icon = self.not_downloaded_icon + return status_icon + + def sort_by_timestamp(self): + self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING) + def on_checkbutton_toggled(self, cellrenderertoggle, path): + iter = self.liststore.get_iter(path) + self.liststore.set_value(iter, self.SELECTED_COL, not cellrenderertoggle.get_active()) + self.rapid_app.set_download_action_sensitivity() + + def set_selected(self, unique_id, value): + iter = self.get_iter_from_unique_id(unique_id) + self.liststore.set_value(iter, self.SELECTED_COL, value) + + def add_file(self, rpd_file, generate_thumbnail): - - def scanMedia(): - """ - Scans media for photos and videos - """ - - # load images to display for when a thumbnail cannot be extracted or created - - self.photoThumbnail = getGenericPhotoImage() - self.videoThumbnail = getGenericVideoImage() - - imageRenameUsesJobCode = rn.usesJobCode(self.prefs.image_rename) - imageSubfolderUsesJobCode = rn.usesJobCode(self.prefs.subfolder) - videoRenameUsesJobCode = rn.usesJobCode(self.prefs.video_rename) - videoSubfolderUsesJobCode = rn.usesJobCode(self.prefs.video_subfolder) - - def loadFileMetadata(mediaFile): - """ - loads the metadate for the file, and additional information if required - """ - - problem = pn.Problem() - try: - mediaFile.loadMetadata() - except: - mediaFile.status = STATUS_CANNOT_DOWNLOAD - mediaFile.metadata = None - problem.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': mediaFile.displayNameCap}) - mediaFile.problem = problem - else: - # generate sample filename and subfolder - if mediaFile.isImage: - fallback_date = None - subfolderPrefsFactory = self.subfolderPrefsFactory - renamePrefsFactory = self.imageRenamePrefsFactory - nameUsesJobCode = imageRenameUsesJobCode - subfolderUsesJobCode = imageSubfolderUsesJobCode - else: - fallback_date = mediaFile.modificationTime - subfolderPrefsFactory = self.videoSubfolderPrefsFactory - renamePrefsFactory = self.videoRenamePrefsFactory - nameUsesJobCode = videoRenameUsesJobCode - subfolderUsesJobCode = videoSubfolderUsesJobCode - - generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - self.prefs.strip_characters, fallback_date) - # generate thumbnail - mediaFile.generateThumbnail(self.videoTempWorkingDir) - - if mediaFile.thumbnail is None: - addGenericThumbnail(mediaFile) - - - def addGenericThumbnail(mediaFile): - """ - Adds a generic thumbnail to the mediafile, which - can be very useful when previews are disabled - """ - mediaFile.genericThumbnail = True - if mediaFile.isImage: - mediaFile.thumbnail = self.photoThumbnail - else: - mediaFile.thumbnail = self.videoThumbnail - - def downloadable(name): - isImage = media.isImage(name) - isVideo = media.isVideo(name) - download = (DOWNLOAD_VIDEO and (isImage or isVideo) or - ((not DOWNLOAD_VIDEO) and isImage)) - return (download, isImage, isVideo) - - def addFile(name, path, size, modificationTime, device, volume, isImage): - """ - Add an image or video to the list of scanned files to be shown to the user for potential downloading - """ - - if isImage: - downloadFolder = self.prefs.download_folder - else: - downloadFolder = self.prefs.video_download_folder - - mediaFile = media.MediaFile(self.thread_id, name, path, size, modificationTime, device, downloadFolder, volume, isImage) - loadFileMetadata(mediaFile) - # modificationTime is very useful for quick sorting - imagesAndVideos.append((mediaFile, modificationTime)) - display_queue.put((self.parentApp.addFile, (mediaFile,))) - - if isImage: - self.noImages += 1 - else: - self.noVideos += 1 + thumbnail_icon = self.get_stock_icon(rpd_file.file_type) + unique_id = rpd_file.unique_id + scan_pid = rpd_file.scan_pid + timestamp = int(rpd_file.modification_time) + + iter = self.liststore.append((thumbnail_icon, + True, + unique_id, + rpd_file.display_name, + timestamp, + rpd_file.file_type, + True, + STATUS_NOT_DOWNLOADED, + self.not_downloaded_icon + )) + + path = self.liststore.get_path(iter) + treerowref = gtk.TreeRowReference(self.liststore, path) + + if scan_pid in self.process_index: + self.process_index[scan_pid].append(unique_id) + else: + self.process_index[scan_pid] = [unique_id,] - def gio_scan(path, fileSizeSum): - """recursive function to scan a directory and its subdirectories - for photos and possibly videos""" - - children = path.enumerate_children('standard::name,standard::type,standard::size,time::modified') + self.treerow_index[unique_id] = treerowref + self.rpd_files[unique_id] = rpd_file + + if generate_thumbnail: + self.total_thumbs_to_generate += 1 - for child in children: - if not self.running: - self.lock.acquire() - self.running = True + def get_sample_file(self, file_type): + """Returns an rpd_file for of a given file type, or None if it does + not exist""" + for unique_id, rpd_file in self.rpd_files.iteritems(): + if rpd_file.file_type == file_type: + if rpd_file.status <> STATUS_CANNOT_DOWNLOAD: + return rpd_file - if not self.ctrl: - return None - - if child.get_file_type() == gio.FILE_TYPE_DIRECTORY: - fileSizeSum = gio_scan(path.get_child(child.get_name()), fileSizeSum) - if fileSizeSum == None: - # this value will be None only if the thread is exiting - return None - elif child.get_file_type() == gio.FILE_TYPE_REGULAR: - name = child.get_name() - download, isImage, isVideo = downloadable(name) - if download: - size = child.get_size() - modificationTime = child.get_modification_time() - addFile(name, path.get_path(), size, modificationTime, self.cardMedia.prettyName(limit=0), self.cardMedia.volume, isImage) - fileSizeSum += size - - return fileSizeSum - - - imagesAndVideos = [] - fileSizeSum = 0 - self.noVideos = 0 - self.noImages = 0 - - if not using_gio or not self.cardMedia.volume: - for root, dirs, files in os.walk(self.cardMedia.getPath()): - for name in files: - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - return None - - - download, isImage, isVideo = downloadable(name) - if download: - fullFileName = os.path.join(root, name) - size = os.path.getsize(fullFileName) - modificationTime = os.path.getmtime(fullFileName) - addFile(name, root, size, modificationTime, self.cardMedia.prettyName(limit=0), self.cardMedia.volume, isImage) - fileSizeSum += size + return None + + def get_unique_id_from_iter(self, iter): + return self.liststore.get_value(iter, 2) + + def get_iter_from_unique_id(self, unique_id): + treerowref = self.treerow_index[unique_id] + path = treerowref.get_path() + return self.liststore.get_iter(path) + + def on_item_activated(self, iconview, path): + """ + """ + iter = self.liststore.get_iter(path) + self.show_preview(iter=iter) + self.advance_get_preview_image(iter) - + + def _get_preview(self, unique_id, rpd_file): + if unique_id not in self.previews_being_fetched: + #check if preview should be from a downloaded file, or the source + if rpd_file.status in DOWNLOADED: + file_location = rpd_file.download_full_file_name else: - # using gio and have a volume - # make call to recursive function to scan volume - fileSizeSum = gio_scan(self.cardMedia.volume.volume.get_root(), fileSizeSum) - if fileSizeSum == None: - # thread exiting - return None - - # sort in place based on modification time - imagesAndVideos.sort(key=operator.itemgetter(1)) - noFiles = len(imagesAndVideos) - - self.scanComplete = True - - self.display_file_types = file_types_by_number(self.noImages, self.noVideos) - - - if noFiles: - self.cardMedia.setMedia(imagesAndVideos, fileSizeSum, noFiles) - # Translators: as already, mentioned the %s value should not be modified or left out. It may be moved if necessary. - # It refers to the actual number of photos that can be copied. For example, the user might see the following: - # '0 of 512 photos' or '0 of 10 videos' or '0 of 202 photos and videos'. - # This particular text is displayed to the user before the download has started. - display = _("%(number)s %(filetypes)s") % {'number':noFiles, 'filetypes':self.display_file_types} - display_queue.put((media_collection_treeview.updateCard, (self.thread_id, self.cardMedia.sizeOfImagesAndVideos()))) - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, display, 0))) - display_queue.put((self.parentApp.setDownloadButtonSensitivity, ())) - - # Translators: as you have already seen, the text can contain values that should not be modified or left out by you, for example %s. - # This text is another example of that, but it is is a little more complex. Here there are two values which will be displayed - # to the user when they run the program, signifying the number of photos found, and the device they were found on. - # %(number)s should be left exactly as is: 'number' should not be translated. The same applies to %(device)s: 'device' should - # not be translated. Generally speaking, if translating the sentence requires it, you can move items like '%(xyz)s' around - # in a sentence, but you should never modify them or leave them out. - cmd_line(_("Device scan complete: found %(number)s %(filetypes)s on %(device)s") % - {'number': noFiles, 'filetypes':self.display_file_types, - 'device': self.cardMedia.prettyName(limit=0)}) - return True + file_location = rpd_file.full_file_name + self.preview_manager.get_preview(unique_id, file_location, + rpd_file.file_type, size_max=None,) + + self.previews_being_fetched.add(unique_id) + + def show_preview(self, unique_id=None, iter=None): + if unique_id is not None: + iter = self.get_iter_from_unique_id(unique_id) + elif iter is not None: + unique_id = self.get_unique_id_from_iter(iter) + else: + # neither an iter or a unique_id were passed + # use iter from first selected file + # if none is selected, choose the first file + selected = self.get_selected_items() + if selected: + path = selected[0] else: - # it might be better to display "0 of 0" here - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - cmd_line(_("Device scan complete: no %(filetypes)s found on %(device)s") % {'device':self.cardMedia.prettyName(limit=0), 'filetypes':self.types_searched_for}) - return False + path = 0 + iter = self.liststore.get_iter(path) + unique_id = self.get_unique_id_from_iter(iter) - def logError(severity, problem, details, resolution=None): - display_queue.put((log_dialog.addMessage, (self.thread_id, severity, problem, details, - resolution))) - if severity == config.WARNING: - self.noWarnings += 1 - else: - self.noErrors += 1 - - def notifyAndUnmount(umountAttemptOK): - if not self.cardMedia.volume: - unmountMessage = "" - notificationName = PROGRAM_NAME + rpd_file = self.rpd_files[unique_id] + + if unique_id in self.previews: + preview_image = self.previews[unique_id] + else: + # request daemon process to get a full size thumbnail + self._get_preview(unique_id, rpd_file) + if unique_id in self.thumbnails: + preview_image = self.thumbnails[unique_id] else: - notificationName = self.cardMedia.volume.get_name() - if self.prefs.auto_unmount and umountAttemptOK: - self.cardMedia.volume.unmount(self.on_volume_unmount) - # This message informs the user that the device (e.g. camera, hard drive or memory card) was automatically unmounted and they can now remove it - unmountMessage = _("The device can now be safely removed") - else: - unmountMessage = "" - - file_types = file_types_by_number(noImagesDownloaded, noVideosDownloaded) - file_types_skipped = file_types_by_number(noImagesSkipped, noVideosSkipped) - message = _("%(noFiles)s %(filetypes)s downloaded") % {'noFiles':noFilesDownloaded, 'filetypes': file_types} - noFilesSkipped = noImagesSkipped + noVideosSkipped - if noFilesSkipped: - message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':noFilesSkipped, 'filetypes':file_types_skipped} + preview_image = self.get_stock_icon(rpd_file.file_type) + + checked = self.liststore.get_value(iter, self.SELECTED_COL) + include_checkbutton_visible = rpd_file.status == STATUS_NOT_DOWNLOADED + self.rapid_app.show_preview_image(unique_id, preview_image, + include_checkbutton_visible, checked) - if self.noWarnings: - message = "%s\n%s " % (message, self.noWarnings) + _("warnings") - if self.noErrors: - message = "%s\n%s " % (message, self.noErrors) + _("errors") - - if unmountMessage: - message = "%s\n%s" % (message, unmountMessage) - - n = pynotify.Notification(notificationName, message) + def _get_next_iter(self, iter): + iter = self.liststore.iter_next(iter) + if iter is None: + iter = self.liststore.get_iter_first() + return iter + + def _get_prev_iter(self, iter): + row = self.liststore.get_path(iter)[0] + if row == 0: + row = len(self.liststore)-1 + else: + row -= 1 + iter = self.liststore.get_iter(row) + return iter + + def show_next_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + iter = self._get_next_iter(iter) + + if iter is not None: + self.show_preview(iter=iter) - if self.cardMedia.volume: - icon = self.cardMedia.volume.get_icon_pixbuf(self.parentApp.notification_icon_size) - else: - icon = self.parentApp.application_icon + # cache next image + self.advance_get_preview_image(iter, prev=False, next=True) - n.set_icon_from_pixbuf(icon) - n.show() + def show_prev_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + iter = self._get_prev_iter(iter) - def createTempDir(baseDir): - """ - Create a temporary directory in which to download the photos to. - - Returns the directory if it was created, else returns None. + if iter is not None: + self.show_preview(iter=iter) - Don't want to put it in system temp folder, as that is likely - to be on another partition and hence copying files from it - to the actual download folder will be slow!""" - try: - t = tempfile.mkdtemp(prefix='rapid-tmp-', - dir=baseDir) - return t - except OSError, (errno, strerror): - if not self.cardMedia.volume: - image_device = _("Source: %s\n") % self.cardMedia.getPath() - else: - _("Device: %s\n") % self.cardMedia.volume.get_name() - destination = _("Destination: %s") % baseDir - logError(config.CRITICAL_ERROR, _('Could not create temporary download directory'), - image_device + destination, - _("Download cannot proceed")) - cmd_line(_("Error:") + " " + _('Could not create temporary download directory')) - cmd_line(image_device + destination) - cmd_line(_("Download cannot proceed")) - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - self.running = False - self.lock.release() - return None + # cache next image + self.advance_get_preview_image(iter, prev=True, next=False) + - def setupBackup(): - """ - Check for presence of backup path or volumes, and return the number of devices being used (1 in case of a path) - """ - no_devices = 0 - if self.prefs.backup_images: - no_devices = len(self.parentApp.backupVolumes) - if not self.prefs.backup_device_autodetection: - if not os.path.isdir(self.prefs.backup_location): - # the user has manually specified a path, but it - # does not exist. This is a problem. - try: - os.makedirs(self.prefs.backup_location) - except: - logError(config.SERIOUS_ERROR, _("Backup path does not exist"), - _("The path %s could not be created") % path, - _("No backups can occur") - ) - no_devices = 0 - return no_devices - - def checkIfNeedAJobCode(): - needAJobCode = NeedAJobCode(self.prefs) + def advance_get_preview_image(self, iter, prev=True, next=True): + unique_ids = [] + if next: + next_iter = self._get_next_iter(iter) + unique_ids.append(self.get_unique_id_from_iter(next_iter)) - for f in self.cardMedia.imagesAndVideos: - mediaFile = f[0] - if mediaFile.status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - if needAJobCode.needAJobCode(mediaFile.jobcode, mediaFile.isImage): - return True - return False + if prev: + prev_iter = self._get_prev_iter(iter) + unique_ids.append(self.get_unique_id_from_iter(prev_iter)) - def createBothTempDirs(): - self.photoTempWorkingDir = createTempDir(photoBaseDownloadDir) - created = self.photoTempWorkingDir is not None - if created and DOWNLOAD_VIDEO: - self.videoTempWorkingDir = createTempDir(videoBaseDownloadDir) - created = self.videoTempWorkingDir is not None - - return created - - - def checkProblemWithNameGeneration(mediaFile): - if mediaFile.problem.has_problem(): - logError(config.WARNING, - mediaFile.problem.get_title(), - _("Source: %(source)s\nDestination: %(destination)s\n%(problem)s") % - {'source': mediaFile.fullFileName, 'destination': mediaFile.downloadFullFileName, 'problem': mediaFile.problem.get_problems()}) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - - def fileAlreadyExists(mediaFile, identifier=None): - """ Notify the user that the photo or video could not be downloaded because it already exists""" + for unique_id in unique_ids: + if not unique_id in self.previews: + rpd_file = self.rpd_files[unique_id] + self._get_preview(unique_id, rpd_file) - # get information on when the existing file was last modified - try: - modificationTime = os.path.getmtime(mediaFile.downloadFullFileName) - dt = datetime.datetime.fromtimestamp(modificationTime) - date = dt.strftime("%x") - time = dt.strftime("%X") - except: - sys.stderr.write("WARNING: could not determine the file modification time of an existing file\n") - date = time = '' - - if not identifier: - mediaFile.problem.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD, {'filetype':mediaFile.displayNameCap}) - mediaFile.problem.add_extra_detail(pn.EXISTING_FILE, {'filetype': mediaFile.displayName, 'date': date, 'time': time}) - mediaFile.status = STATUS_DOWNLOAD_FAILED - log_status = config.SERIOUS_ERROR - problem_text = pn.extra_detail_definitions[pn.EXISTING_FILE] % {'date':date, 'time':time, 'filetype': mediaFile.displayName} - else: - mediaFile.problem.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED, {'filetype':mediaFile.displayNameCap}) - mediaFile.problem.add_extra_detail(pn.UNIQUE_IDENTIFIER, {'identifier': identifier, 'filetype': mediaFile.displayName, 'date': date, 'time': time}) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - log_status = config.WARNING - problem_text = pn.extra_detail_definitions[pn.UNIQUE_IDENTIFIER] % {'identifier': identifier, 'filetype': mediaFile.displayName, 'date': date, 'time': time} - - logError(log_status, mediaFile.problem.get_title(), - _("Source: %(source)s\nDestination: %(destination)s") - % {'source': mediaFile.fullFileName, 'destination': mediaFile.downloadFullFileName}, - problem_text) - - def downloadCopyingError(mediaFile, inst=None, errno=None, strerror=None): - """Notify the user that an error occurred (most likely at the OS / filesystem level) when coyping a photo or video""" + def check_all(self, check_all, file_type=None): + for row in self.liststore: + if row[self.CHECKBUTTON_VISIBLE_COL]: + if file_type is not None: + if row[self.FILETYPE_COL] == file_type: + row[self.SELECTED_COL] = check_all + else: + row[self.SELECTED_COL] = check_all + self.rapid_app.set_download_action_sensitivity() - if errno != None and strerror != None: - mediaFile.problem.add_problem(None, pn.DOWNLOAD_COPYING_ERROR_W_NO, {'filetype': mediaFile.displayName}) - mediaFile.problem.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, {'errorno': errno, 'strerror': strerror}) - - else: - mediaFile.problem.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': mediaFile.displayName}) - if not inst: - # hopefully inst will never be None, but just to be safe... - inst = _("Please check your system and try again.") - mediaFile.problem.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_DETAIL, inst) - - logError(config.SERIOUS_ERROR, mediaFile.problem.get_title(), mediaFile.problem.get_problems()) - mediaFile.status = STATUS_DOWNLOAD_FAILED + def files_are_checked_to_download(self): + """ + Returns True if there is any file that the user has indicated they + intend to download, else returns False. + """ + for row in self.liststore: + if row[self.SELECTED_COL]: + rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] + if rpd_file.status not in DOWNLOADED: + return True + return False + + def get_files_checked_for_download(self, scan_pid): + """ + Returns a dict of scan ids and associated files the user has indicated + they want to download + + If scan_pid is not None, then returns only those files from that scan_pid + """ + files = dict() + if scan_pid is None: + for row in self.liststore: + if row[self.SELECTED_COL]: + rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] + if rpd_file.status not in DOWNLOADED: + scan_pid = rpd_file.scan_pid + if scan_pid in files: + files[scan_pid].append(rpd_file) + else: + files[scan_pid] = [rpd_file,] + else: + files[scan_pid] = [] + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if rpd_file.status not in DOWNLOADED: + iter = self.get_iter_from_unique_id(unique_id) + if self.liststore.get_value(iter, self.SELECTED_COL): + files[scan_pid].append(rpd_file) + return files - def sameNameDifferentExif(image_name, mediaFile): - """Notify the user that a file was already downloaded with the same name, but the exif information was different""" - i1_ext, i1_date_time, i1_subseconds = downloaded_files.extExifDateTime(image_name) - detail = {'image1': "%s%s" % (image_name, i1_ext), - 'image1_date': i1_date_time.strftime("%x"), - 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds), - 'image2': mediaFile.name, - 'image2_date': mediaFile.metadata.dateTime().strftime("%x"), - 'image2_time': time_subseconds_human_readable( - mediaFile.metadata.dateTime(), - mediaFile.metadata.subSeconds())} - mediaFile.problem.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail) - - msg = pn.problem_definitions[pn.SAME_FILE_DIFFERENT_EXIF][1] % detail - logError(config.WARNING,_('Photos detected with the same filenames, but taken at different times'), msg) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - - def generateSubfolderAndFileName(mediaFile): - """ - Generates subfolder and file names for photos and videos - """ - - skipFile = alreadyDownloaded = False - sequence_to_use = None - - if mediaFile.isVideo: - fileRenameFactory = self.videoRenamePrefsFactory - subfolderFactory = self.videoSubfolderPrefsFactory - else: - # file is an photo - fileRenameFactory = self.imageRenamePrefsFactory - subfolderFactory = self.subfolderPrefsFactory + def get_no_files_remaining(self, scan_pid): + """ + Returns the number of files that have not yet been downloaded for the + scan_pid + """ + i = 0 + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if rpd_file.status == STATUS_NOT_DOWNLOADED: + i += 1 + return i + + def files_remain_to_download(self): + """ + Returns True if any files remain that are not downloaded, else returns + False + """ + for row in self.liststore: + if row[self.DOWNLOAD_STATUS_COL] == STATUS_NOT_DOWNLOADED: + return True + return False - fileRenameFactory.setJobCode(mediaFile.jobcode) - subfolderFactory.setJobCode(mediaFile.jobcode) - mediaFile.problem = pn.Problem() - subfolderFactory.initializeProblem(mediaFile.problem) - fileRenameFactory.initializeProblem(mediaFile.problem) - - # Here we cannot assume that the subfolder value will contain something -- the user may have changed the preferences after the scan - mediaFile.downloadSubfolder = subfolderFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, - self.stripCharacters, fallback_date = mediaFile.modificationTime) - - - if self.prefs.synchronize_raw_jpg and usesImageSequenceElements and mediaFile.isImage: - #synchronizing RAW and JPEG only applies to photos, not videos - image_name, image_ext = os.path.splitext(mediaFile.name) - with self.downloadedFilesLock: - i, sequence_to_use = downloaded_files.matching_pair(image_name, image_ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds()) - if i == -1: - # this exact file has already been downloaded (same extension, same filename, and same exif date time subsecond info) - if not addUniqueIdentifier: - logError(config.SERIOUS_ERROR,_('Photo has already been downloaded'), - _("Source: %(source)s") % {'source': mediaFile.fullFileName}) - mediaFile.problem.add_problem(None, pn.FILE_ALREADY_DOWNLOADED, {'filetype': mediaFile.displayNameCap}) - skipFile = True - + def mark_download_pending(self, files_by_scan_pid): + """ + Sets status to download pending and updates thumbnails display + """ + for scan_pid in files_by_scan_pid: + for rpd_file in files_by_scan_pid[scan_pid]: + unique_id = rpd_file.unique_id + self.rpd_files[unique_id].status = STATUS_DOWNLOAD_PENDING + iter = self.get_iter_from_unique_id(unique_id) + if not self.rapid_app.auto_start_is_on: + # don't make the checkbox invisible immediately when on auto start + # otherwise the box can be rendred at the wrong size, as it is + # realized after the checkbox has already been made invisible + self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) + self.liststore.set_value(iter, self.SELECTED_COL, False) + self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING) + icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING) + self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) - # pass the subfolder the image will go into, as this is needed to determine subfolder sequence numbers - # indicate that sequences chosen should be queued + def select_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + path = self.liststore.get_path(iter) + self.select_path(path) + self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5) + + def get_stock_icon(self, file_type): + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.stock_photo_thumbnails.stock_thumbnail_image_icon + else: + return self.stock_video_thumbnails.stock_thumbnail_image_icon - if not skipFile: - mediaFile.downloadName = fileRenameFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - sequencesPreliminary = True, - sequence_to_use = sequence_to_use, - fallback_date = mediaFile.modificationTime) - - mediaFile.downloadPath = os.path.join(mediaFile.downloadFolder, mediaFile.downloadSubfolder) - mediaFile.downloadFullFileName = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - - if not mediaFile.downloadName or not mediaFile.downloadSubfolder: - if not mediaFile.downloadName and not mediaFile.downloadSubfolder: - area = _("subfolder and filename") - elif not mediaFile.downloadName: - area = _("filename") - else: - area = _("subfolder") - problem.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': mediaFile.displayNameCap, 'area': area}) - problem.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - skipFile = True - logError(config.SERIOUS_ERROR, pn.problem_definitions[ERROR_IN_NAME_GENERATION][1] % {'filetype': mediaFile.displayNameCap, 'area': area}) + def update_status_post_download(self, rpd_file): + iter = self.get_iter_from_unique_id(rpd_file.unique_id) + self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status) + icon = self.get_status_icon(rpd_file.status) + self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) + self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) + self.rpd_files[rpd_file.unique_id] = rpd_file - if not skipFile: - checkProblemWithNameGeneration(mediaFile) - else: - self.sizeDownloaded += mediaFile.size * (no_backup_devices + 1) - mediaFile.status = STATUS_DOWNLOAD_FAILED - - return (skipFile, sequence_to_use) + def generate_thumbnails(self, scan_pid): + """Initiate thumbnail generation for files scanned in one process + """ + rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] + self.thumbnail_manager.add_task(rpd_files) + + def update_thumbnail(self, thumbnail_data): + """ + Takes the generated thumbnail and updates the display - def progress_callback(amount_downloaded, total): - if (amount_downloaded - self.bytes_downloaded > 2097152) or (amount_downloaded == total): - chunk_downloaded = amount_downloaded - self.bytes_downloaded - self.bytes_downloaded = amount_downloaded - percentComplete = (float(self.sizeDownloaded + amount_downloaded) / sizeFiles) * 100 - - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, None, chunk_downloaded))) + If the thumbnail_data includes a second image, that is used to + update the thumbnail list using the unique_id + """ + unique_id = thumbnail_data[0] + thumbnail_icon = thumbnail_data[1] - def downloadFile(mediaFile, sequence_to_use): - """ - Downloads the photo or video file to the specified subfolder - """ + if thumbnail_icon is not None: + # get the thumbnail icon in PIL format + thumbnail_icon = thumbnail_icon.get_image() - if not mediaFile.isImage: - renameFactory = self.videoRenamePrefsFactory - else: - renameFactory = self.imageRenamePrefsFactory + treerowref = self.treerow_index[unique_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) - def progress_callback_no_update(amount_downloaded, total): - pass + if thumbnail_icon: + self.liststore.set(iter, 0, thumbnail_icon) - try: - fileDownloaded = False - if not os.path.isdir(mediaFile.downloadPath): - os.makedirs(mediaFile.downloadPath) - - nameUniqueBeforeCopy = True - downloadNonUniqueFile = True - - # do a preliminary check to see if a file with the same name already exists - if os.path.exists(mediaFile.downloadFullFileName): - nameUniqueBeforeCopy = False - if not addUniqueIdentifier: - downloadNonUniqueFile = False - if (usesVideoSequenceElements and not mediaFile.isImage) or (usesImageSequenceElements and mediaFile.isImage and not self.prefs.synchronize_raw_jpg): - # potentially, a unique file name could still be generated - # investigate this possibility - with self.fileSequenceLock: - for possibleName in renameFactory.generateNameSequencePossibilities( - mediaFile.metadata, - mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - fallback_date = mediaFile.modificationTime): - if possibleName: - # no need to check for any problems here, it's just a temporary name - possibleFile = os.path.join(mediaFile.downloadPath, possibleName) - possibleTempFile = os.path.join(tempWorkingDir, possibleName) - if not os.path.exists(possibleFile) and not os.path.exists(possibleTempFile): - downloadNonUniqueFile = True - break + if len(thumbnail_data) > 2: + # get the 2nd image in PIL format + self.thumbnails[unique_id] = thumbnail_data[2].get_image() - - if not downloadNonUniqueFile: - fileAlreadyExists(mediaFile) - - copy_succeeded = False - if nameUniqueBeforeCopy or downloadNonUniqueFile: - tempWorkingfile = os.path.join(tempWorkingDir, mediaFile.downloadName) - if using_gio: - g_dest = gio.File(path=tempWorkingfile) - g_src = gio.File(path=mediaFile.fullFileName) - try: - if not g_src.copy(g_dest, progress_callback, cancellable=gio.Cancellable()): - downloadCopyingError(mediaFile) - else: - copy_succeeded = True - except glib.GError, inst: - downloadCopyingError(mediaFile, inst=inst) - else: - shutil.copy2(mediaFile.fullFileName, tempWorkingfile) - copy_succeeded = True - - if copy_succeeded: - with self.fileRenameLock: - doRename = True - if usesSequenceElements: - with self.fileSequenceLock: - # get a filename and use this as the "real" filename - if sequence_to_use is None and self.prefs.synchronize_raw_jpg and mediaFile.isImage: - # must check again, just in case the matching pair has been downloaded in the meantime - image_name, image_ext = os.path.splitext(mediaFile.name) - with self.downloadedFilesLock: - i, sequence_to_use = downloaded_files.matching_pair(image_name, image_ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds()) - if i == -99: - sameNameDifferentExif(image_name, mediaFile) - - mediaFile.downloadName = renameFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - sequencesPreliminary = False, - sequence_to_use = sequence_to_use, - fallback_date = mediaFile.modificationTime) - - if not mediaFile.downloadName: - # there was a serious error generating the filename - doRename = False - else: - mediaFile.downloadFullFileName = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - # check if the file exists again - if os.path.exists(mediaFile.downloadFullFileName): - if not addUniqueIdentifier: - doRename = False - fileAlreadyExists(mediaFile) - else: - # add basic suffix to make the filename unique - name = os.path.splitext(mediaFile.downloadName) - suffixAlreadyUsed = True - while suffixAlreadyUsed: - if mediaFile.downloadFullFileName in duplicate_files: - duplicate_files[mediaFile.downloadFullFileName] += 1 - else: - duplicate_files[mediaFile.downloadFullFileName] = 1 - identifier = '_%s' % duplicate_files[mediaFile.downloadFullFileName] - mediaFile.downloadName = name[0] + identifier + name[1] - possibleNewFile = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - suffixAlreadyUsed = os.path.exists(possibleNewFile) - - fileAlreadyExists(mediaFile, identifier) - mediaFile.downloadFullFileName = possibleNewFile - - - if doRename: - rename_succeeded = False - if using_gio: - g_dest = gio.File(path=mediaFile.downloadFullFileName) - g_src = gio.File(path=tempWorkingfile) - try: - if not g_src.move(g_dest, progress_callback_no_update, cancellable=gio.Cancellable()): - downloadCopyingError(mediaFile) - else: - rename_succeeded = True - except glib.GError, inst: - downloadCopyingError(mediaFile, inst=inst) - else: - os.rename(tempWorkingfile, mediaFile.downloadFullFileName) - rename_succeeded = True - - if rename_succeeded: - fileDownloaded = True - if mediaFile.status != STATUS_DOWNLOADED_WITH_WARNING: - mediaFile.status = STATUS_DOWNLOADED - if usesImageSequenceElements: - if self.prefs.synchronize_raw_jpg and mediaFile.isImage: - name, ext = os.path.splitext(mediaFile.name) - if sequence_to_use is None: - with self.fileSequenceLock: - seq = renameFactory.sequences.getFinalSequence() - else: - seq = sequence_to_use - with self.downloadedFilesLock: - downloaded_files.add_download(name, ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds(), seq) - - - with self.fileSequenceLock: - if sequence_to_use is None: - renameFactory.sequences.imageCopySucceeded() - if usesStoredSequenceNo: - self.prefs.stored_sequence_no += 1 - - with self.fileSequenceLock: - if sequence_to_use is None: - if self.prefs.incrementDownloadsToday(): - # A new day, according the user's preferences of what time a day begins, has started - cmd_line(_("New day has started - resetting 'Downloads Today' sequence number")) - - sequences.setDownloadsToday(0) - - except (IOError, OSError), (errno, strerror): - downloadCopyingError(mediaFile, errno=errno, strerror=strerror) - - if usesSequenceElements: - if not fileDownloaded and sequence_to_use is None: - renameFactory.sequences.imageCopyFailed() - #update record keeping using in tracking progress - self.sizeDownloaded += mediaFile.size - self.bytes_downloaded_in_download = self.bytes_downloaded - - return fileDownloaded + def thumbnail_results(self, source, condition): + connection = self.thumbnail_manager.get_pipe(source) + + conn_type, data = connection.recv() + + if conn_type == rpdmp.CONN_COMPLETE: + connection.close() + return False + else: - - def backupFile(mediaFile, fileDownloaded, no_backup_devices): - """ - Backup photo or video to path(s) chosen by the user + for thumbnail_data in data: + self.update_thumbnail(thumbnail_data) - there are three scenarios: - (1) file has just been downloaded and should now be backed up - (2) file was already downloaded on some previous occassion and should still be backed up, because it hasn't been yet - (3) file has been backed up already (or at least, a file with the same name already exists) + self.thumbnails_generated += len(data) - A backup medium can be used to backup photos or videos, or both. - """ + # clear progress bar information if all thumbnails have been + # extracted + if self.thumbnails_generated == self.total_thumbs_to_generate: + self.rapid_app.download_progressbar.set_fraction(0.0) + self.rapid_app.download_progressbar.set_text('') + self.thumbnails_generated = 0 + self.total_thumbs_to_generate = 0 - backed_up = False - fileNotBackedUpMessageDisplayed = False - error_encountered = False - expected_bytes_downloaded = self.sizeDownloaded + no_backup_devices * mediaFile.size + else: + self.rapid_app.download_progressbar.set_fraction( + float(self.thumbnails_generated) / self.total_thumbs_to_generate) - if no_backup_devices: - for rootBackupDir in self.parentApp.backupVolumes: - self.bytes_downloaded = 0 - if self.prefs.backup_device_autodetection: - volume = self.parentApp.backupVolumes[rootBackupDir].get_name() - if mediaFile.isImage: - backupDir = os.path.join(rootBackupDir, self.prefs.backup_identifier) - else: - backupDir = os.path.join(rootBackupDir, self.prefs.video_backup_identifier) - else: - # photos and videos will be backed up into the same root folder, which the user has manually specified - backupDir = rootBackupDir - volume = backupDir # os.path.split(backupDir)[1] - - # if user has chosen auto detection, then: - # photos should only be backed up to photo backup locations - # videos should only be backed up to video backup locations - # if user did not choose autodetection, and the backup path doesn't exist, then - # will try to create it - if os.path.isdir(backupDir) or not self.prefs.backup_device_autodetection: - - backupPath = os.path.join(backupDir, mediaFile.downloadSubfolder) - newBackupFile = os.path.join(backupPath, mediaFile.downloadName) - copyBackup = True - if os.path.exists(newBackupFile): - # this check is of course not thread safe -- it doesn't need to be, because at this stage the file names are going to be unique - # (the folder structure is the same as the actual download folders, and the file names are unique in them) - copyBackup = self.prefs.backup_duplicate_overwrite - - if copyBackup: - mediaFile.problem.add_problem(None, pn.BACKUP_EXISTS_OVERWRITTEN, volume) - else: - mediaFile.problem.add_problem(None, pn.BACKUP_EXISTS, volume) - severity = config.SERIOUS_ERROR - fileNotBackedUpMessageDisplayed = True - - title = _("Backup of %(file_type)s already exists") % {'file_type': mediaFile.displayName} - details = _("Source: %(source)s\nDestination: %(destination)s") \ - % {'source': mediaFile.fullFileName, 'destination': newBackupFile} - if copyBackup: - resolution = _("Backup %(file_type)s overwritten") % {'file_type': mediaFile.displayName} - else: - if self.prefs.backup_device_autodetection: - volume = self.parentApp.backupVolumes[rootBackupDir].get_name() - resolution = _("%(file_type)s not backed up to %(volume)s") % {'file_type': mediaFile.displayNameCap, 'volume': volume} - else: - resolution = _("%(file_type)s not backed up") % {'file_type': mediaFile.displayNameCap} - logError(severity, title, details, resolution) - - if copyBackup: - if fileDownloaded: - fileToCopy = mediaFile.downloadFullFileName - else: - fileToCopy = mediaFile.fullFileName - if os.path.isdir(backupPath): - pathExists = True - else: - pathExists = False - # create the backup subfolders - if using_gio: - dirs = gio.File(backupPath) - try: - if dirs.make_directory_with_parents(cancellable=gio.Cancellable()): - pathExists = True - except glib.GError, inst: - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Destination directory could not be created: %(directory)s\n") % - {'directory': backupPath, } + - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': mediaFile.fullFileName, 'destination': newBackupFile} + "\n" + - _("Error: %(inst)s") % {'inst': inst}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - else: - # recreate folder structure in backup location - # cannot do os.makedirs(backupPath) - it can give bad results when using external drives - # we know backupDir exists - # all the components of subfolder may not - folders = mediaFile.downloadSubfolder.split(os.path.sep) - folderToMake = backupDir - for f in folders: - if f: - folderToMake = os.path.join(folderToMake, f) - if not os.path.isdir(folderToMake): - try: - os.mkdir(folderToMake) - pathExists = True - except (IOError, OSError), (errno, strerror): - fileNotBackedUpMessageDisplayed = True - inst = "%s: %s" % (errno, strerror) - mediaFile.problem.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Destination directory could not be created: %(directory)s\n") % - {'directory': backupPath, } + - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': mediaFile.fullFileName, 'destination': newBackupFile} + "\n" + - _("Error: %(errno)s %(strerror)s") % {'errno': errno, 'strerror': strerror}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - - break - - if pathExists: - if using_gio: - g_dest = gio.File(path=newBackupFile) - g_src = gio.File(path=fileToCopy) - if self.prefs.backup_duplicate_overwrite: - flags = gio.FILE_COPY_OVERWRITE - else: - flags = gio.FILE_COPY_NONE - try: - if not g_src.copy(g_dest, progress_callback, flags, cancellable=gio.Cancellable()): - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - error_encountered = True - else: - backed_up = True - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_problem(None, pn.NO_DOWNLOAD_WAS_BACKED_UP, volume) - except glib.GError, inst: - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': fileToCopy, 'destination': newBackupFile} + "\n" + - _("Error: %(inst)s") % {'inst': inst}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - else: - try: - shutil.copy2(fileToCopy, newBackupFile) - backed_up = True - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_problem(None, pn.NO_DOWNLOAD_WAS_BACKED_UP, volume) - - except (IOError, OSError), (errno, strerror): - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - inst = "%s: %s" % (errno, strerror) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': fileToCopy, 'destination': newBackupFile} + "\n" + - _("Error: %(errno)s %(strerror)s") % {'errno': errno, 'strerror': strerror}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) + + return True + + def preview_results(self, unique_id, preview_full_size, preview_small): + """ + Receive a full size preview image and update + """ + self.previews_being_fetched.remove(unique_id) + if preview_full_size: + preview_image = preview_full_size.get_image() + self.previews[unique_id] = preview_image + self.rapid_app.update_preview_image(unique_id, preview_image) - #update record keeping using in tracking progress - self.sizeDownloaded += mediaFile.size - self.bytes_downloaded_in_backup += self.bytes_downloaded - - if not backed_up and not fileNotBackedUpMessageDisplayed: - # The file has not been backed up to any medium - mediaFile.problem.add_problem(None, pn.NO_BACKUP_PERFORMED, {'filetype': mediaFile.displayNameCap}) - - severity = config.SERIOUS_ERROR - problem = _("%(file_type)s could not be backed up") % {'file_type': mediaFile.displayName} - details = _("Source: %(source)s") % {'source': mediaFile.fullFileName} - if self.prefs.backup_device_autodetection: - resolution = _("No suitable backup volume was found") - else: - resolution = _("A backup location was not found") - logError(severity, problem, details, resolution) - - if backed_up and mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_extra_detail(pn.BACKUP_OK_TYPE, mediaFile.displayNameCap) - - if not backed_up: - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.status = STATUS_DOWNLOAD_AND_BACKUP_FAILED - else: - mediaFile.status = STATUS_BACKUP_PROBLEM - elif error_encountered: - # it was backed up to at least one volume, but there was an error on another backup volume - if mediaFile.status != STATUS_DOWNLOAD_FAILED: - mediaFile.status = STATUS_BACKUP_PROBLEM - - # Take into account instances where a backup device has been removed part way through a download - # (thereby making self.parentApp.backupVolumes have less items than expected) - if self.sizeDownloaded < expected_bytes_downloaded: - self.sizeDownloaded = expected_bytes_downloaded - return backed_up - + + def clear_all(self, scan_pid=None, keep_downloaded_files=False): + """ + Removes files from display and internal tracking. - self.hasStarted = True - display_queue.open('w') - - #Do not try to handle any preference errors here - getPrefs(False) + If scan_pid is not None, then only files matching that scan_pid will + be removed. Otherwise, everything will be removed. - #Check photo and video download path, create if necessary - photoBaseDownloadDir = self.prefs.download_folder - if not checkDownloadPath(photoBaseDownloadDir): - return # cleanup already done + If keep_downloaded_files is True, files will not be removed if they + have been downloaded. + """ + if scan_pid is None and not keep_downloaded_files: + self.liststore.clear() + self.treerow_index = {} + self.process_index = {} + + self.rpd_files = {} + else: + if scan_pid in self.process_index: + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if not keep_downloaded_files or not rpd_file.status in DOWNLOADED: + treerowref = self.treerow_index[rpd_file.unique_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + self.liststore.remove(iter) + del self.treerow_index[rpd_file.unique_id] + del self.rpd_files[rpd_file.unique_id] + if not keep_downloaded_files or not len(self.process_index[scan_pid]): + del self.process_index[scan_pid] + +class TaskManager: + def __init__(self, results_callback, batch_size): + self.results_callback = results_callback + + # List of actual process, it's terminate_queue, and it's run_event + self._processes = [] + + self._pipes = {} + self.batch_size = batch_size + + self.paused = False + self.no_tasks = 0 + + + def add_task(self, task): + pid = self._setup_task(task) + logger.debug("TaskManager PID: %s", pid) + self.no_tasks += 1 + return pid - if DOWNLOAD_VIDEO: - videoBaseDownloadDir = self.prefs.video_download_folder - if not checkDownloadPath(videoBaseDownloadDir): - return - else: - videoBaseDownloadDir = self.videoTempWorkingDir = None - - if not createBothTempDirs(): - return - - s = scanMedia() - if s is None: - if not self.ctrl: - self.running = False - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - display_queue.close("rw") - return - else: - sys.stderr.write("FIXME: scan returned None, but the thread is not meant to be exiting\n") - if not s: - cmd_line(_("This device has no %(types_searched_for)s to download from.") % {'types_searched_for': self.types_searched_for}) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - self.running = False - return - if self.scanResultsStale or self.scanResultsStaleDownloadFolder: - display_queue.put((self.parentApp.regenerateScannedDevices, (self.thread_id, ))) - all_files_downloaded = False + def _setup_task(self, task): + task_results_conn, task_process_conn = Pipe(duplex=False) - totalNonErrorFiles = self.cardMedia.numberOfFilesNotCannotDownload() + source = task_results_conn.fileno() + self._pipes[source] = task_results_conn + gobject.io_add_watch(source, gobject.IO_IN, self.results_callback) - if not self.autoStart: - # halt thread, waiting to be restarted so download proceeds - self.cleanUp() - self.running = False - self.lock.acquire() - - if not self.ctrl: - # thread will restart at this point, when the program is exiting - # so must exit if self.ctrl indicates this - - self.running = False - display_queue.close("rw") - return - - self.running = True - if not createBothTempDirs(): - return - - else: - if need_job_code_for_renaming: - if checkIfNeedAJobCode(): - if job_code == None: - self.cleanUp() - self.waitingForJobCode = True - display_queue.put((self.parentApp.getJobCode, ())) - self.running = False - self.lock.acquire() - - if not self.ctrl: - # thread is exiting - display_queue.close("rw") - return - - self.running = True - self.waitingForJobCode = False - if not createBothTempDirs(): - return - else: - # User has entered a job code, and it's in the global variable - # Assign it to all those files that do not have one - display_queue.put((self.parentApp.selection_vbox.selection_treeview.apply_job_code, (job_code, False, True, self.thread_id))) - - # auto start could be false if the user hit cancel when prompted for a job code - if self.autoStart: - # set all in this thread to download pending - display_queue.put((self.parentApp.selection_vbox.selection_treeview.set_status_to_download_pending, (False, self.thread_id))) - # wait until all the files have had their status set to download pending, and once that is done, restart - self.running = False - self.lock.acquire() - self.running = True - - # set download started time - display_queue.put((self.parentApp.setDownloadStartTime, ())) - - while not all_files_downloaded: - - # set the download start time to be the time that the user clicked the download button, or if on auto start, the value just set - i = 0 - while self.parentApp.download_start_time is None or i > 2: - time.sleep(0.5) - i += 1 - - if self.parentApp.download_start_time: - start_time = self.parentApp.download_start_time - else: - # in a bizarre corner case situation, with mulitple cards of greatly varying size, - # it's possible the start time was set above and then in the meantime unset (very unlikely, but conceivably it could happen) - # fall back to the current time in this less than satisfactory situation - start_time = datetime.datetime.now() - - self.imageRenamePrefsFactory.setDownloadStartTime(start_time) - self.subfolderPrefsFactory.setDownloadStartTime(start_time) - if DOWNLOAD_VIDEO: - self.videoRenamePrefsFactory.setDownloadStartTime(start_time) - self.videoSubfolderPrefsFactory.setDownloadStartTime(start_time) - - self.noErrors = self.noWarnings = 0 - - if not getPrefs(True): - self.running = False - display_queue.close("rw") - return - - self.downloadStarted = True - cmd_line(_("Download has started from %s") % self.cardMedia.prettyName(limit=0)) - - - noFiles, sizeFiles, fileIndex = self.cardMedia.sizeAndNumberDownloadPending() - cmd_line(_("Attempting to download %s files") % noFiles) - - no_backup_devices = setupBackup() - - # include the time it takes to copy to the backup volumes - sizeFiles = sizeFiles * (no_backup_devices + 1) - - display_queue.put((self.parentApp.timeRemaining.set, (self.thread_id, sizeFiles))) - - i = 0 - self.sizeDownloaded = noFilesDownloaded = noImagesDownloaded = noVideosDownloaded = noImagesSkipped = noVideosSkipped = 0 - filesDownloadedSuccessfully = [] - self.bytes_downloaded_in_backup = 0 - - display_queue.put((self.parentApp.addToTotalDownloadSize, (sizeFiles, ))) - display_queue.put((self.parentApp.setOverallDownloadMark, ())) - display_queue.put((self.parentApp.postStartDownloadTasks, ())) - - sizeFiles = float(sizeFiles) - - addUniqueIdentifier = self.prefs.download_conflict_resolution == config.ADD_UNIQUE_IDENTIFIER - usesImageSequenceElements = self.imageRenamePrefsFactory.usesSequenceElements() - usesVideoSequenceElements = self.videoRenamePrefsFactory.usesSequenceElements() - usesSequenceElements = usesVideoSequenceElements or usesImageSequenceElements - - usesStoredSequenceNo = (self.imageRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER) or - self.videoRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER)) - sequences.setUseOfSequenceElements( - self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SESSION_SEQ_NUMBER), - self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SEQUENCE_LETTER)) - - # reset the progress bar to update the status of this download attempt - progressBarText = _("%(number)s of %(total)s %(filetypes)s") % {'number': 0, 'total': noFiles, 'filetypes':self.display_file_types} - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, progressBarText, 0))) - - - while i < noFiles: - # if the user pauses the download, then this will be triggered - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - self.running = False - self.cleanUp() - display_queue.close("rw") - return - - # get information about the image to deduce image name and path - mediaFile = self.cardMedia.imagesAndVideos[fileIndex[i]][0] - if not mediaFile.status == STATUS_DOWNLOAD_PENDING: - sys.stderr.write("FIXME: Thread %s is trying to download a file that it should not be!!" % self.thread_id) - else: - self.bytes_downloaded_in_download = self.bytes_downloaded_in_backup = self.bytes_downloaded = 0 - if mediaFile.isImage: - tempWorkingDir = self.photoTempWorkingDir - baseDownloadDir = photoBaseDownloadDir - else: - tempWorkingDir = self.videoTempWorkingDir - baseDownloadDir = videoBaseDownloadDir - - skipFile, sequence_to_use = generateSubfolderAndFileName(mediaFile) - - if skipFile: - if mediaFile.isImage: - noImagesSkipped += 1 - else: - noVideosSkipped += 1 - else: - fileDownloaded = downloadFile(mediaFile, sequence_to_use) - - if self.prefs.backup_images: - backed_up = backupFile(mediaFile, fileDownloaded, no_backup_devices) - - if fileDownloaded: - noFilesDownloaded += 1 - if mediaFile.isImage: - noImagesDownloaded += 1 - else: - noVideosDownloaded += 1 - if self.prefs.backup_images and backed_up: - filesDownloadedSuccessfully.append(mediaFile.fullFileName) - elif not self.prefs.backup_images: - filesDownloadedSuccessfully.append(mediaFile.fullFileName) - else: - if mediaFile.isImage: - noImagesSkipped += 1 - else: - noVideosSkipped += 1 - - #update the selction treeview in the main window with the new status of the file - display_queue.put((self.parentApp.update_status_post_download, (mediaFile.treerowref, ))) - - percentComplete = (float(self.sizeDownloaded) / sizeFiles) * 100 - - if self.sizeDownloaded == sizeFiles and (totalNonErrorFiles - noFiles): - progressBarText = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % { - 'number': i + 1, 'total': noFiles, 'filetypes':self.display_file_types, - 'remaining': totalNonErrorFiles - noFiles} - else: - progressBarText = _("%(number)s of %(total)s %(filetypes)s") % {'number': i + 1, 'total': noFiles, 'filetypes':self.display_file_types} - - if using_gio: - # do not want to update the progress bar any more than it has already been updated - size = mediaFile.size * (no_backup_devices + 1) - self.bytes_downloaded_in_download - self.bytes_downloaded_in_backup - else: - size = mediaFile.size * (no_backup_devices + 1) - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, progressBarText, size))) - - i += 1 - - with self.statsLock: - self.downloadStats.adjust(self.sizeDownloaded, noImagesDownloaded, noVideosDownloaded, noImagesSkipped, noVideosSkipped, self.noWarnings, self.noErrors) - - if self.prefs.auto_delete: - j = 0 - for imageOrVideo in filesDownloadedSuccessfully: - try: - os.unlink(imageOrVideo) - j += 1 - except OSError, (errno, strerror): - logError(config.SERIOUS_ERROR, _("Could not delete photo or video from device"), - _("Photo: %(source)s\nError: %(errno)s %(strerror)s") - % {'source': image, 'errno': errno, 'strerror': strerror}) - except: - logError(config.SERIOUS_ERROR, _("Could not delete photo or video from device"), - _("Photo: %(source)s")) - - cmd_line(_("Deleted %(number)i %(filetypes)s from device") % {'number':j, 'filetypes':self.display_file_types}) - - totalNonErrorFiles = totalNonErrorFiles - noFiles - if totalNonErrorFiles == 0: - all_files_downloaded = True - - # must manually delete these variables, or else the media cannot be unmounted (bug in some versions of pyexiv2 / exiv2) - # for some reason directories on the device remain open with read only access, even after these steps - I don't know why - del self.subfolderPrefsFactory, self.imageRenamePrefsFactory, self.videoSubfolderPrefsFactory, self.videoRenamePrefsFactory - for i in self.cardMedia.imagesAndVideos: - i[0].metadata = None - - notifyAndUnmount(umountAttemptOK = all_files_downloaded) - cmd_line(_("Download complete from %s") % self.cardMedia.prettyName(limit=0)) - display_queue.put((self.parentApp.notifyUserAllDownloadsComplete,())) - display_queue.put((self.parentApp.resetSequences,())) - - if all_files_downloaded: - self.downloadComplete = True - else: - self.cleanUp() - self.downloadStarted = False - self.running = False - self.lock.acquire() - if not self.ctrl: - # thread will restart at this point, when the program is exiting - # so must exit if self.ctrl indicates this - - self.running = False - display_queue.close("rw") - return - self.running = True - if not createBothTempDirs(): - return - - - display_queue.put((self.parentApp.exitOnDownloadComplete, ())) - display_queue.close("rw") - - self.cleanUp() - - self.running = False - if noFiles: - self.lock.release() + terminate_queue = Queue() + run_event = Event() + run_event.set() + + return self._initiate_task(task, task_process_conn, terminate_queue, run_event) + + def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): + logger.error("Implement child class method!") - def startStop(self): - if self.isAlive(): - if self.running: - self.running = False - else: - try: - self.lock.release() + def processes(self): + for i in range(len(self._processes)): + yield self._processes[i] - except thread_error: - sys.stderr.write(str(self.thread_id) + " thread error\n") + def start(self): + self.paused = False + for scan in self.processes(): + run_event = scan[2] + if not run_event.is_set(): + run_event.set() + + def pause(self): + self.paused = True + for scan in self.processes(): + run_event = scan[2] + if run_event.is_set(): + run_event.clear() - def cleanUp(self): + def request_termination(self): """ - Deletes temporary files and folders + Send a signal to processes that they should immediately terminate """ - - for tempWorkingDir in (self.videoTempWorkingDir, self.photoTempWorkingDir): - if tempWorkingDir: - # possibly delete any lingering files - if os.path.isdir(tempWorkingDir): - tf = os.listdir(tempWorkingDir) - if tf: - for f in tf: - os.remove(os.path.join(tempWorkingDir, f)) - os.rmdir(tempWorkingDir) + requested = False + for p in self.processes(): + if p[0].is_alive(): + requested = True + p[1].put(None) + # The process might be paused: let it run + run_event = p[2] + if not run_event.is_set(): + run_event.set() - def quit(self): - """ - Quits the thread - - A thread can be in one of four states: - - Not started (not alive, nothing to do) - Started and actively running (alive) - Started and paused (alive) - Completed (not alive, nothing to do) + return requested + + def terminate_forcefully(self): """ + Forcefully terminates any running processes. Use with great caution. + No cleanup action is performed. - # cleanup any temporary directories and files - self.cleanUp() + As python essential reference (4th edition) says, if the process + 'holds a lock or is involved with interprocess communication, + terminating it might cause a deadlock or corrupted I/O.' + """ - if self.hasStarted: - if self.isAlive(): - self.ctrl = False - - if not self.running: - released = False - while not released: - try: - self.lock.release() - released = True - except thread_error: - sys.stderr.write("Could not release lock for thread %s\n" % self.thread_id) - - + for p in self.processes(): + if p[0].is_alive(): + p[0].terminate() - def on_volume_unmount(self, data1, data2): - """ needed for call to unmount volume""" - pass + + def get_pipe(self, source): + return self._pipes[source] + + def get_no_active_processes(self): + """ + Returns how many processes are currently active, i.e. running + """ + i = 0 + for p in self.processes(): + if p[0].is_alive(): + i += 1 + return i -class MediaTreeView(gtk.TreeView): +class ScanManager(TaskManager): + + def __init__(self, results_callback, batch_size, generate_folder, + add_device_function): + TaskManager.__init__(self, results_callback, batch_size) + self.add_device_function = add_device_function + self.generate_folder = generate_folder + + def _initiate_task(self, device, task_process_conn, terminate_queue, run_event): + scan = scan_process.Scan(device.get_path(), self.batch_size, self.generate_folder, + task_process_conn, terminate_queue, run_event) + scan.start() + self._processes.append((scan, terminate_queue, run_event)) + self.add_device_function(scan.pid, device, + # This refers to when a device like a hard drive is having its contents scanned, + # looking for photos or videos. It is visible initially in the progress bar for each device + # (which normally holds "x photos and videos"). + # It maybe displayed only briefly if the contents of the device being scanned is small. + progress_bar_text=_('scanning...')) + + return scan.pid + +class CopyFilesManager(TaskManager): + + def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): + photo_download_folder = task[0] + video_download_folder = task[1] + scan_pid = task[2] + files = task[3] + generate_thumbnails = task[4] + + copy_files = copyfiles.CopyFiles(photo_download_folder, + video_download_folder, + files, generate_thumbnails, + scan_pid, self.batch_size, + task_process_conn, terminate_queue, run_event) + copy_files.start() + self._processes.append((copy_files, terminate_queue, run_event)) + return copy_files.pid + +class ThumbnailManager(TaskManager): + + def _initiate_task(self, files, task_process_conn, terminate_queue, run_event): + generator = tn.GenerateThumbnails(files, self.batch_size, task_process_conn, terminate_queue, run_event) + generator.start() + self._processes.append((generator, terminate_queue, run_event)) + return generator.pid + + +class SingleInstanceTaskManager: """ - TreeView display of devices and associated copying progress. + Base class to manage single instance processes. Examples are daemon + processes, but also a non-daemon process that has one simple task. - Assumes a threaded environment. + Core (infrastructure) functionality is implemented in this class. + Derived classes should implemented functionality to actually implement + specific tasks. """ - def __init__(self, parentApp): - - self.parentApp = parentApp - # device name, size of images on the device (human readable), copy progress (%), copy text - self.liststore = gtk.ListStore(str, str, float, str) - self.mapThreadToRow = {} - - gtk.TreeView.__init__(self, self.liststore) - - self.props.enable_search = False - # make it impossible to select a row - selection = self.get_selection() - selection.set_mode(gtk.SELECTION_NONE) + def __init__(self, results_callback): + self.results_callback = results_callback - # Device refers to a thing like a camera, memory card in its reader, external hard drive, Portable Storage Device, etc. - column0 = gtk.TreeViewColumn(_("Device"), gtk.CellRendererText(), - text=0) - self.append_column(column0) - - # Size refers to the total size of images on the device, typically in MB or GB - column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=1) - self.append_column(column1) + self.task_results_conn, self.task_process_conn = Pipe(duplex=True) - column2 = gtk.TreeViewColumn(_("Download Progress"), - gtk.CellRendererProgress(), value=2, text=3) - self.append_column(column2) - self.show_all() + source = self.task_results_conn.fileno() + gobject.io_add_watch(source, gobject.IO_IN, self.task_results) + - def addCard(self, thread_id, cardName, sizeFiles, progress = 0.0, progressBarText = ''): +class PreviewManager(SingleInstanceTaskManager): + def __init__(self, results_callback): + SingleInstanceTaskManager.__init__(self, results_callback) + self._get_preview = tn.GetPreviewImage(self.task_process_conn) + self._get_preview.start() - # add the row, and get a temporary pointer to the row - iter = self.liststore.append((cardName, sizeFiles, progress, progressBarText)) + def get_preview(self, unique_id, full_file_name, file_type, size_max): + self.task_results_conn.send((unique_id, full_file_name, file_type, size_max)) - self._setThreadMap(thread_id, iter) + def task_results(self, source, condition): + unique_id, preview_full_size, preview_small = self.task_results_conn.recv() + self.results_callback(unique_id, preview_full_size, preview_small) + return True - # adjust scrolled window height, based on row height and number of ready to start downloads - if workers.noReadyToStartWorkers() >= 1 or workers.noRunningWorkers() > 0: - # please note, at program startup, self.rowHeight() will be less than it will be when already running - # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program - # (with one card at startup), it could be 21 - height = (workers.noReadyToStartWorkers() + workers.noRunningWorkers() + 2) * (self.rowHeight()) - self.parentApp.media_collection_scrolledwindow.set_size_request(-1, height) - +class SubfolderFileManager(SingleInstanceTaskManager): + """ + Manages the daemon process that renames files and creates subfolders + """ + def __init__(self, results_callback, sequence_values): + SingleInstanceTaskManager.__init__(self, results_callback) + self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, sequence_values) + self._subfolder_file.start() + logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid) + + def rename_file_and_move_to_subfolder(self, download_succeeded, + download_count, rpd_file): + + self.task_results_conn.send((download_succeeded, download_count, + rpd_file)) + logger.debug("Download count: %s.", download_count) + + + def task_results(self, source, condition): + move_succeeded, rpd_file = self.task_results_conn.recv() + self.results_callback(move_succeeded, rpd_file) + return True - def updateCard(self, thread_id, totalSizeFiles): - """ - Updates the size of the photos and videos on the device, displayed to the user - """ - if thread_id in self.mapThreadToRow: - iter = self._getThreadMap(thread_id) - self.liststore.set_value(iter, 1, totalSizeFiles) - else: - sys.stderr.write("FIXME: this card is unknown") - - def removeCard(self, thread_id): - if thread_id in self.mapThreadToRow: - iter = self._getThreadMap(thread_id) - self.liststore.remove(iter) - del self.mapThreadToRow[thread_id] - def _setThreadMap(self, thread_id, iter): - """ - convert the temporary iter into a tree reference, which is - permanent - """ - - path = self.liststore.get_path(iter) - treerowRef = gtk.TreeRowReference(self.liststore, path) - self.mapThreadToRow[thread_id] = treerowRef - - def _getThreadMap(self, thread_id): - """ - return the tree iter for this thread - """ +class ResizblePilImage(gtk.DrawingArea): + def __init__(self, bg_color=None): + gtk.DrawingArea.__init__(self) + self.base_image = None + self.bg_color = bg_color + self.connect('expose_event', self.expose) - if thread_id in self.mapThreadToRow: - treerowRef = self.mapThreadToRow[thread_id] - path = treerowRef.get_path() - iter = self.liststore.get_iter(path) - return iter - else: - return None - - def updateProgress(self, thread_id, percentComplete, progressBarText, bytesDownloaded): + def set_image(self, image): + self.base_image = image - iter = self._getThreadMap(thread_id) - if iter: - self.liststore.set_value(iter, 2, percentComplete) - if progressBarText: - self.liststore.set_value(iter, 3, progressBarText) - if percentComplete or bytesDownloaded: - self.parentApp.updateOverallProgress(thread_id, bytesDownloaded, percentComplete) + #set up sizes and ratio used for drawing the derived image + self.base_image_w = self.base_image.size[0] + self.base_image_h = self.base_image.size[1] + self.base_image_aspect = float(self.base_image_w) / self.base_image_h + self.queue_draw() + + def expose(self, widget, event): - def rowHeight(self): - if not self.mapThreadToRow: - return 0 - else: - index = self.mapThreadToRow.keys()[0] - path = self.mapThreadToRow[index].get_path() - col = self.get_column(0) - return self.get_background_area(path, col)[3] + 1 - - -class ShowWarningDialog(gtk.Dialog): - """ - Displays a warning to the user that downloading directly from a - camera does not always work well - """ - def __init__(self, parent_window, postChoiceCB): - gtk.Dialog.__init__(self, _("Downloading From Cameras"), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_OK, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB + cairo_context = self.window.cairo_create() - primary_msg = _("Downloading directly from a camera may work poorly or not at all") - secondary_msg = _("Downloading from a card reader always works and is generally much faster. It is strongly recommended to use a card reader.") + x = event.area.x + y = event.area.y + w = event.area.width + h = event.area.height - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - - primary_label = gtk.Label() - primary_label.set_markup("<b>%s</b>" % primary_msg) - primary_label.set_line_wrap(True) - primary_label.set_alignment(0, 0.5) - - secondary_label = gtk.Label() - secondary_label.set_text(secondary_msg) - secondary_label.set_line_wrap(True) - secondary_label.set_alignment(0, 0.5) - - self.show_again_checkbutton = gtk.CheckButton(_('_Show this message again'), True) - self.show_again_checkbutton.set_active(True) + #constrain operations to event area + cairo_context.rectangle(x, y, w, h) + cairo_context.clip_preserve() - msg_vbox = gtk.VBox() - msg_vbox.pack_start(primary_label, False, False, padding=6) - msg_vbox.pack_start(secondary_label, False, False, padding=6) - msg_vbox.pack_start(self.show_again_checkbutton) + #set background color, if needed + if self.bg_color: + cairo_context.set_source_rgb(*self.bg_color) + cairo_context.fill_preserve() - icon = parent_window.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) - image = gtk.Image() - image.set_from_pixbuf(icon) - image.set_alignment(0, 0) + if not self.base_image: + return False - warning_hbox = gtk.HBox() - warning_hbox.pack_start(image, False, False, padding = 12) - warning_hbox.pack_start(msg_vbox, False, False, padding = 12) + frame_aspect = float(w) / h + + if frame_aspect > self.base_image_aspect: + # Frame is wider than image + height = h + width = int(height * self.base_image_aspect) + else: + # Frame is taller than image + width = w + height = int(width / self.base_image_aspect) - self.vbox.pack_start(warning_hbox, padding=6) + #resize image + pil_image = self.base_image.copy() + if self.base_image_w < width or self.base_image_h < height: + logger.debug("Upsizing image") + pil_image = tn.upsize_pil(pil_image, (width, height)) + else: + logger.debug("Downsizing image") + tn.downsize_pil(pil_image, (width, height)) - self.set_border_width(6) - self.set_has_separator(False) + #image width and height + image_w = pil_image.size[0] + image_h = pil_image.size[1] - self.set_default_response(gtk.RESPONSE_OK) - - self.set_transient_for(parent_window) - self.show_all() - - self.connect('response', self.on_response) + #center the image horizontally and vertically + #top left and right corners for the image: + image_x = x + ((w - image_w) / 2) + image_y = y + ((h - image_h) / 2) - def on_response(self, device_dialog, response): - show_again = self.show_again_checkbutton.get_active() - self.postChoiceCB(self, show_again) + image = create_cairo_image_surface(pil_image, image_w, image_h) + cairo_context.set_source_surface(image, image_x, image_y) + cairo_context.paint() -class UseDeviceDialog(gtk.Dialog): - def __init__(self, parent_window, path, volume, autostart, postChoiceCB): - gtk.Dialog.__init__(self, _('Device Detected'), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, - gtk.STOCK_YES, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB - - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - prompt_label = gtk.Label(_('Should this device or partition be used to download photos or videos from?')) - prompt_label.set_line_wrap(True) - prompt_hbox = gtk.HBox() - prompt_hbox.pack_start(prompt_label, False, False, padding=6) - device_label = gtk.Label() - device_label.set_markup("<b>%s</b>" % volume.get_name(limit=0)) - device_hbox = gtk.HBox() - device_hbox.pack_start(device_label, False, False) - path_label = gtk.Label() - path_label.set_markup("<i>%s</i>" % path) - path_hbox = gtk.HBox() - path_hbox.pack_start(path_label, False, False) - - icon = volume.get_icon_pixbuf(36) - if icon: - image = gtk.Image() - image.set_from_pixbuf(icon) - - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - self.always_checkbutton = gtk.CheckButton(_('_Remember this choice'), True) - - if icon: - device_hbox_icon = gtk.HBox(homogeneous=False, spacing=6) - device_hbox_icon.pack_start(image, False, False, padding = 6) - device_vbox = gtk.VBox(homogeneous=True, spacing=6) - device_vbox.pack_start(device_hbox, False, False) - device_vbox.pack_start(path_hbox, False, False) - device_hbox_icon.pack_start(device_vbox, False, False) - self.vbox.pack_start(device_hbox_icon, padding = 6) - else: - self.vbox.pack_start(device_hbox, padding=6) - self.vbox.pack_start(path_hbox, padding = 6) - - self.vbox.pack_start(prompt_hbox, padding=6) - self.vbox.pack_start(self.always_checkbutton, padding=6) + return False + + - self.set_border_width(6) - self.set_has_separator(False) +class PreviewImage: + + def __init__(self, parent_app, builder): + #set background color to equivalent of '#444444 + self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267)) + self.preview_image_eventbox = builder.get_object("preview_eventbox") + self.preview_image_eventbox.add(self.preview_image) + self.preview_image.show() + self.download_this_checkbutton = builder.get_object("download_this_checkbutton") + self.rapid_app = parent_app - self.set_default_response(gtk.RESPONSE_OK) - - - self.set_transient_for(parent_window) - self.show_all() - self.path = path - self.volume = volume - self.autostart = autostart - - self.connect('response', self.on_response) - - def on_response(self, device_dialog, response): - userSelected = False - permanent_choice = self.always_checkbutton.get_active() - if response == gtk.RESPONSE_OK: - userSelected = True - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("%s selected for downloading from" % self.volume.get_name(limit=0))) - if permanent_choice: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("This device or partition will always be used to download from")) - else: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("%s rejected as a download device" % self.volume.get_name(limit=0))) - if permanent_choice: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("This device or partition will never be used to download from")) - - self.postChoiceCB(self, userSelected, permanent_choice, self.path, - self.volume, self.autostart) - -class RemoveAllJobCodeDialog(gtk.Dialog): - def __init__(self, parent_window, postChoiceCB): - gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, - gtk.STOCK_YES, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + self.base_preview_image = None # large size image used to scale down from + self.current_preview_size = (0,0) + self.preview_image_size_limit = (0,0) - prompt_hbox = gtk.HBox() + self.unique_id = None - icontheme = gtk.icon_theme_get_default() - icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) - if icon: - image = gtk.Image() - image.set_from_pixbuf(icon) - prompt_hbox.pack_start(image, False, False, padding = 6) - - prompt_label = gtk.Label(_('Should all Job Codes be removed?')) - prompt_label.set_line_wrap(True) - prompt_hbox.pack_start(prompt_label, False, False, padding=6) - - self.vbox.pack_start(prompt_hbox, padding=6) + def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None, + checked=None): + """ + """ + self.preview_image.set_image(pil_image) + self.unique_id = unique_id + if checked is not None: + self.download_this_checkbutton.set_active(checked) + self.download_this_checkbutton.grab_focus() - self.set_border_width(6) - self.set_has_separator(False) + if include_checkbutton_visible is not None: + self.download_this_checkbutton.props.visible = include_checkbutton_visible - self.set_default_response(gtk.RESPONSE_OK) + def update_preview_image(self, unique_id, pil_image): + if unique_id == self.unique_id: + self.set_preview_image(unique_id, pil_image) - - self.set_transient_for(parent_window) - self.show_all() - self.connect('response', self.on_response) +class RapidApp(dbus.service.Object): + """ + The main Rapid Photo Downloader application class. + + Contains functionality for main program window, and directs all other + processes. + """ + + def __init__(self, bus, path, name, taskserver=None): - def on_response(self, device_dialog, response): - userSelected = response == gtk.RESPONSE_OK - self.postChoiceCB(self, userSelected) + dbus.service.Object.__init__ (self, bus, path, name) + self.running = False + self.taskserver = taskserver -class JobCodeDialog(gtk.Dialog): - """ Dialog prompting for a job code""" - - def __init__(self, parent_window, job_codes, default_job_code, postJobCodeEntryCB, autoStart, downloadSelected, entryOnly): - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - gtk.Dialog.__init__(self, _('Enter a Job Code'), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_OK, gtk.RESPONSE_OK)) - + # Setup program preferences, and set callback for when they change + self._init_prefs() - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - self.postJobCodeEntryCB = postJobCodeEntryCB - self.autoStart = autoStart - self.downloadSelected = downloadSelected + # Initialize widgets in the main window, and variables that point to them + self._init_widgets() + self._init_pynotify() - self.combobox = gtk.combo_box_entry_new_text() - for text in job_codes: - self.combobox.append_text(text) - - self.job_code_hbox = gtk.HBox(homogeneous = False) + # Initialize job code handling + self._init_job_code() - if len(job_codes) and not entryOnly: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - task_label = gtk.Label(_('Enter a new Job Code, or select a previous one')) - else: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - task_label = gtk.Label(_('Enter a new Job Code')) - task_label.set_line_wrap(True) - task_hbox = gtk.HBox() - task_hbox.pack_start(task_label, False, False, padding=6) - - label = gtk.Label(_('Job Code:')) - self.job_code_hbox.pack_start(label, False, False, padding=6) - self.job_code_hbox.pack_start(self.combobox, True, True, padding=6) + # Remember the window size from the last time the program was run, or + # set a default size + self._set_window_size() - self.set_border_width(6) - self.set_has_separator(False) - - # make entry box have entry completion - self.entry = self.combobox.child + # Setup various widgets + self._setup_buttons() + self._setup_error_icons() + self._setup_icons() + + # Show the main window + self.rapidapp.show() - completion = gtk.EntryCompletion() - completion.set_match_func(self.match_func) - completion.connect("match-selected", - self.on_completion_match) - completion.set_model(self.combobox.get_model()) - completion.set_text_column(0) - self.entry.set_completion(completion) + # Check program preferences - don't allow auto start if there is a problem + prefs_valid, msg = prefsrapid.check_prefs_for_validity(self.prefs) + if not prefs_valid: + self.notify_prefs_are_invalid(details=msg) - # when user hits enter, close the dialog window - self.set_default_response(gtk.RESPONSE_OK) - self.entry.set_activates_default(True) - - if default_job_code: - self.entry.set_text(default_job_code) + # Initialize variables with which to track important downloads results + self._init_download_tracking() - self.vbox.pack_start(task_hbox, False, False, padding = 6) - self.vbox.pack_start(self.job_code_hbox, False, False, padding=12) + # Set up process managers. + # A task such as scanning a device or copying files is handled in its + # own process. + self._start_process_managers() - self.set_transient_for(parent_window) - self.show_all() - self.connect('response', self.on_job_code_resp) - - def match_func(self, completion, key, iter): - model = completion.get_model() - return model[iter][0].lower().startswith(self.entry.get_text().lower()) - - def on_completion_match(self, completion, model, iter): - self.entry.set_text(model[iter][0]) - self.entry.set_position(-1) - - def get_job_code(self): - return self.combobox.child.get_text() + # Setup devices from which to download from and backup to + self.setup_devices(on_startup=True, on_preference_change=False, + block_auto_start=not prefs_valid) - def on_job_code_resp(self, jc_dialog, response): - userChoseCode = False - if response == gtk.RESPONSE_OK: - userChoseCode = True - cmd_line(_("Job Code entered")) - else: - cmd_line(_("Job Code not entered")) - self.postJobCodeEntryCB(self, userChoseCode, self.get_job_code(), self.autoStart, self.downloadSelected) - + # Ensure the device collection scrolled window is not too small + self._set_device_collection_size() + + def on_rapidapp_destroy(self, widget, data=None): + self._terminate_processes(terminate_file_copies = True) -class SelectionTreeView(gtk.TreeView): - """ - TreeView display of photos and videos available for download - - Assumes a threaded environment. - """ - def __init__(self, parentApp): + # save window and component sizes + self.prefs.vpaned_pos = self.main_vpaned.get_position() - self.parentApp = parentApp - self.rapidApp = parentApp.parentApp + x, y, width, height = self.rapidapp.get_allocation() + self.prefs.main_window_size_x = width + self.prefs.main_window_size_y = height - self.liststore = gtk.ListStore( - gtk.gdk.Pixbuf, # 0 thumbnail icon - str, # 1 name (for sorting) - int, # 2 timestamp (for sorting), float converted into an int - str, # 3 date (human readable) - int, # 4 size (for sorting) - str, # 5 size (human readable) - int, # 6 isImage (for sorting) - gtk.gdk.Pixbuf, # 7 type (photo or video) - str, # 8 job code - gobject.TYPE_PYOBJECT, # 9 mediaFile (for data) - gtk.gdk.Pixbuf, # 10 status icon - int, # 11 status (downloaded, cannot download, etc, for sorting) - str, # 12 path (on the device) - str, # 13 device - int) # 14 thread id (worker the file is associated with) - - self.selected_rows = set() - - # sort by date (unless there is a problem) - self.liststore.set_sort_column_id(2, gtk.SORT_ASCENDING) + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - gtk.TreeView.__init__(self, self.liststore) - - selection = self.get_selection() - selection.set_mode(gtk.SELECTION_MULTIPLE) - selection.connect('changed', self.on_selection_changed) - - self.set_rubber_banding(True) - - # Status Column - # Indicates whether file was downloaded, or a warning or error of some kind - cell = gtk.CellRendererPixbuf() - cell.set_property("yalign", 0.5) - status_column = gtk.TreeViewColumn(_("Status"), cell, pixbuf=10) - status_column.set_sort_column_id(11) - status_column.connect('clicked', self.header_clicked) - self.append_column(status_column) - - # Type of file column i.e. photo or video (displays at user request) - cell = gtk.CellRendererPixbuf() - cell.set_property("yalign", 0.5) - self.type_column = gtk.TreeViewColumn(_("Type"), cell, pixbuf=7) - self.type_column.set_sort_column_id(6) - self.type_column.set_clickable(True) - self.type_column.connect('clicked', self.header_clicked) - self.append_column(self.type_column) - self.display_type_column(self.rapidApp.prefs.display_type_column) - - #File thumbnail column - if not DOWNLOAD_VIDEO: - title = _("Photo") - else: - title = _("File") - thumbnail_column = gtk.TreeViewColumn(title) - cellpb = gtk.CellRendererPixbuf() - if not DROP_SHADOW: - cellpb.set_fixed_size(60,50) - thumbnail_column.pack_start(cellpb, False) - thumbnail_column.set_attributes(cellpb, pixbuf=0) - thumbnail_column.set_sort_column_id(1) - thumbnail_column.set_clickable(True) - thumbnail_column.connect('clicked', self.header_clicked) - self.append_column(thumbnail_column) - - # Job code column - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.job_code_column = gtk.TreeViewColumn(_("Job Code"), cell, text=8) - self.job_code_column.set_sort_column_id(8) - self.job_code_column.set_resizable(True) - self.job_code_column.set_clickable(True) - self.job_code_column.connect('clicked', self.header_clicked) - self.append_column(self.job_code_column) - - # Date column - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - date_column = gtk.TreeViewColumn(_("Date"), cell, text=3) - date_column.set_sort_column_id(2) - date_column.set_resizable(True) - date_column.set_clickable(True) - date_column.connect('clicked', self.header_clicked) - self.append_column(date_column) - - # Size column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.size_column = gtk.TreeViewColumn(_("Size"), cell, text=5) - self.size_column.set_sort_column_id(4) - self.size_column.set_resizable(True) - self.size_column.set_clickable(True) - self.size_column.connect('clicked', self.header_clicked) - self.append_column(self.size_column) - self.display_size_column(self.rapidApp.prefs.display_size_column) - - # Device column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.device_column = gtk.TreeViewColumn(_("Device"), cell, text=13) - self.device_column.set_sort_column_id(13) - self.device_column.set_resizable(True) - self.device_column.set_clickable(True) - self.device_column.connect('clicked', self.header_clicked) - self.append_column(self.device_column) - self.display_device_column(self.rapidApp.prefs.display_device_column) - - # Filename column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.filename_column = gtk.TreeViewColumn(_("Filename"), cell, text=1) - self.filename_column.set_sort_column_id(1) - self.filename_column.set_resizable(True) - self.filename_column.set_clickable(True) - self.filename_column.connect('clicked', self.header_clicked) - self.append_column(self.filename_column) - self.display_filename_column(self.rapidApp.prefs.display_filename_column) - - # Path column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.path_column = gtk.TreeViewColumn(_("Path"), cell, text=12) - self.path_column.set_sort_column_id(12) - self.path_column.set_resizable(True) - self.path_column.set_clickable(True) - self.path_column.connect('clicked', self.header_clicked) - self.append_column(self.path_column) - self.display_path_column(self.rapidApp.prefs.display_path_column) - - self.show_all() + gtk.main_quit() - # flag used to determine if a preview should be generated or not - # there is no point generating a preview for each photo when - # select all photos is called, for instance - self.suspend_previews = False + def _terminate_processes(self, terminate_file_copies=False): + + # FIXME: need more fine grained tuning here - must cancel large file + # copies midstream + if terminate_file_copies: + logger.info("Terminating all processes...") + + scan_termination_requested = self.scan_manager.request_termination() + thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() + if terminate_file_copies: + copy_files_termination_requested = self.copy_files_manager.request_termination() + else: + copy_files_termination_requested = False + + if scan_termination_requested or thumbnails_termination_requested: + time.sleep(1) + if (self.scan_manager.get_no_active_processes() > 0 or + self.thumbnails.thumbnail_manager.get_no_active_processes() > 0): + time.sleep(1) + # must try again, just in case a new scan has meanwhile started! + self.scan_manager.request_termination() + self.thumbnails.thumbnail_manager.terminate_forcefully() + self.scan_manager.terminate_forcefully() + + if terminate_file_copies and copy_files_termination_requested: + time.sleep(1) + self.copy_files_manager.terminate_forcefully() + + if terminate_file_copies: + self._clean_all_temp_dirs() + + # # # + # Events and tasks related to displaying preview images and thumbnails + # # # + + def on_download_this_checkbutton_toggled(self, checkbutton): + value = checkbutton.get_active() + self.thumbnails.set_selected(self.preview_image.unique_id, value) + self.set_download_action_sensitivity() + + def on_preview_eventbox_button_press_event(self, widget, event): + + if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: + self.show_thumbnails() + + def on_show_thumbnails_action_activate(self, action): + logger.debug("on_show_thumbnails_action_activate") + self.show_thumbnails() + + def on_show_image_action_activate(self, action): + logger.debug("on_show_image_action_activate") + self.thumbnails.show_preview() + + def on_check_all_action_activate(self, action): + self.thumbnails.check_all(check_all=True) + + def on_uncheck_all_action_activate(self, action): + self.thumbnails.check_all(check_all=False) + + def on_check_all_photos_action_activate(self, action): + self.thumbnails.check_all(check_all=True, + file_type=rpdfile.FILE_TYPE_PHOTO) + + def on_check_all_videos_action_activate(self, action): + self.thumbnails.check_all(check_all=True, + file_type=rpdfile.FILE_TYPE_VIDEO) + + def on_quit_action_activate(self, action): + self.on_rapidapp_destroy(widget=self.rapidapp, data=None) + + def on_refresh_action_activate(self, action): + self.setup_devices(on_startup=False, on_preference_change=False, + block_auto_start=True) + + def on_get_help_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/help.html") - self.user_has_clicked_header = False + def on_about_action_activate(self, action): + self.about.set_property("name", PROGRAM_NAME) + self.about.set_property("version", utilities.human_readable_version( + __version__)) + self.about.run() + self.about.destroy() + + def on_report_problem_action_activate(self, action): + webbrowser.open("https://bugs.launchpad.net/rapid") + + def on_translate_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/translate.html") + + def on_donate_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/donate.html") + + def show_preview_image(self, unique_id, image, include_checkbutton_visible, checked): + if self.main_notebook.get_current_page() == 0: # thumbnails + logger.debug("Switching to preview image display") + self.main_notebook.set_current_page(1) + self.preview_image.set_preview_image(unique_id, image, include_checkbutton_visible, checked) + self.next_image_action.set_sensitive(True) + self.prev_image_action.set_sensitive(True) - # icons to be displayed in status column + def update_preview_image(self, unique_id, image): + self.preview_image.update_preview_image(unique_id, image) + + def show_thumbnails(self): + logger.debug("Switching to thumbnails display") + self.main_notebook.set_current_page(0) + self.thumbnails.select_image(self.preview_image.unique_id) + self.next_image_action.set_sensitive(False) + self.prev_image_action.set_sensitive(False) + + + def on_next_image_action_activate(self, action): + if self.preview_image.unique_id is not None: + self.thumbnails.show_next_image(self.preview_image.unique_id) + + def on_prev_image_action_activate(self, action): + if self.preview_image.unique_id is not None: + self.thumbnails.show_prev_image(self.preview_image.unique_id) + + def set_thumbnail_sort(self): + """ + If all the scans are complete, sets the sort order + """ + if self.scan_manager.get_no_active_processes() == 0: + self.thumbnails.sort_by_timestamp() - self.downloaded_icon = self.render_icon('rapid-photo-downloader-downloaded', gtk.ICON_SIZE_MENU) - self.download_failed_icon = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.error_icon = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.warning_icon = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) - self.download_pending_icon = self.render_icon('rapid-photo-downloader-download-pending', gtk.ICON_SIZE_MENU) - self.downloaded_with_warning_icon = self.render_icon('rapid-photo-downloader-downloaded-with-warning', gtk.ICON_SIZE_MENU) - self.downloaded_with_error_icon = self.render_icon('rapid-photo-downloader-downloaded-with-error', gtk.ICON_SIZE_MENU) + # # # + # Volume management + # # # + + def start_volume_monitor(self): + if not self.vmonitor: + self.vmonitor = gio.volume_monitor_get() + self.vmonitor.connect("mount-added", self.on_mount_added) + self.vmonitor.connect("mount-removed", self.on_mount_removed) + + + def setup_devices(self, on_startup, on_preference_change, block_auto_start): + """ - # make the not yet downloaded icon a transparent square - self.not_downloaded_icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 16, 16) - self.not_downloaded_icon.fill(0xffffffff) - self.not_downloaded_icon = self.not_downloaded_icon.add_alpha(True, chr(255), chr(255), chr(255)) - # but make it be a tick in the preview pane - self.not_downloaded_icon_tick = self.render_icon(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) - - #preload generic icons - self.icon_photo = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo24.png')) - self.icon_video = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video24.png')) - #with shadows - if DROP_SHADOW: - self.generic_photo_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo_small_shadow.png')) - self.generic_video_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video_small_shadow.png')) - self.iconDropShadow = DropShadow(offset=(3,3), shadow = (0x34, 0x34, 0x34, 0xff), border=6) - else: - self.generic_photo_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo_small.png')) - self.generic_video_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video_small.png')) + Setup devices from which to download from and backup to - self.previewed_file_treerowref = None - self.icontheme = gtk.icon_theme_get_default() + Sets up volumes for downloading from and backing up to + on_startup should be True if the program is still starting, + i.e. this is being called from the program's initialization. + on_preference_change should be True if this is being called as the + result of a preference being changed - def get_thread(self, iter): - """ - Returns the thread associated with the liststore's iter - """ - return self.liststore.get_value(iter, 14) + block_auto_start should be True if automation options to automatically + start a download should be ignored - def get_status(self, iter): - """ - Returns the status associated with the liststore's iter + Removes any image media that are currently not downloaded, + or finished downloading """ - return self.liststore.get_value(iter, 11) - def get_mediaFile(self, iter): - """ - Returns the mediaFile associated with the liststore's iter - """ - return self.liststore.get_value(iter, 9) + if self.using_volume_monitor(): + self.start_volume_monitor() - def get_is_image(self, iter): - """ - Returns the file type (is image or video) associated with the liststore's iter - """ - return self.liststore.get_value(iter, 6) - - def get_type_icon(self, iter): - """ - Returns the file type's pixbuf associated with the liststore's iter - """ - return self.liststore.get_value(iter, 7) + + self.clear_non_running_downloads() - def get_job_code(self, iter): - """ - Returns the job code associated with the liststore's iter - """ - return self.liststore.get_value(iter, 8) + mounts = [] + self.backup_devices = {} - def get_status_icon(self, status, preview=False): - """ - Returns the correct icon, based on the status - """ - if status == STATUS_WARNING: - status_icon = self.warning_icon - elif status == STATUS_CANNOT_DOWNLOAD: - status_icon = self.error_icon - elif status == STATUS_DOWNLOADED: - status_icon = self.downloaded_icon - elif status == STATUS_NOT_DOWNLOADED: - if preview: - status_icon = self.not_downloaded_icon_tick - else: - status_icon = self.not_downloaded_icon - elif status in [STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]: - status_icon = self.downloaded_with_warning_icon - elif status in [STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOAD_AND_BACKUP_FAILED]: - status_icon = self.downloaded_with_error_icon - elif status == STATUS_DOWNLOAD_PENDING: - status_icon = self.download_pending_icon - else: - sys.stderr.write("FIXME: unknown status: %s\n" % status) - status_icon = self.not_downloaded_icon - return status_icon + # Clear download statistics and tracking + # FIXME - def get_tree_row_refs(self): - """ - Returns a list of all tree row references - """ - tree_row_refs = [] - iter = self.liststore.get_iter_first() - while iter: - tree_row_refs.append(self.get_mediaFile(iter).treerowref) - iter = self.liststore.iter_next(iter) - return tree_row_refs - - def get_selected_tree_row_refs(self): - """ - Returns a list of tree row references for the selected rows - """ - tree_row_refs = [] - selection = self.get_selection() - model, pathlist = selection.get_selected_rows() - for path in pathlist: - iter = self.liststore.get_iter(path) - tree_row_refs.append(self.get_mediaFile(iter).treerowref) - return tree_row_refs - - def get_tree_row_iters(self, selected_only=False): - """ - Yields tree row iters + if self.using_volume_monitor(): + # either using automatically detected backup devices + # or download devices + for mount in self.vmonitor.get_mounts(): + if not mount.is_shadowed(): + path = mount.get_root().get_path() + if path: + if (path in self.prefs.device_blacklist and + self.search_for_PSD()): + logger.info("%s ignored", mount.get_name()) + else: + logger.info("Detected %s", mount.get_name()) + is_backup_mount = self.check_if_backup_mount(path) + if is_backup_mount: + self.backup_devices[path] = mount + elif (self.prefs.device_autodetection and + (dv.is_DCIM_device(path) or + self.search_for_PSD())): + mounts.append((path, mount)) + + + if not self.prefs.device_autodetection: + # user manually specified the path from which to download + path = self.prefs.device_location + if path: + logger.info("Using manually specified path %s", path) + if utilities.is_directory(path): + mounts.append((path, None)) + else: + logger.error("Download path does not exist: %s", path) + + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + # will backup to this path, but don't need any volume info + # associated with it + self.backup_devices[self.prefs.backup_location] = None - If selected_only is True, then only those from the selected - rows will be returned. + # Display amount of free space in a status bar message + self.display_free_space() - This function is essential when modifying any content - in the list store (because rows can easily be moved when their - content changes) - """ - if selected_only: - tree_row_refs = self.get_selected_tree_row_refs() + if block_auto_start: + self.auto_start_is_on = False else: - tree_row_refs = self.get_tree_row_refs() - for reference in tree_row_refs: - path = reference.get_path() - yield self.liststore.get_iter(path) - - def add_file(self, mediaFile): - if debug_info: - cmd_line('Adding file %s' % mediaFile.fullFileName) - - # metadata is loaded when previews are generated before downloading - if mediaFile.metadata: - date = mediaFile.dateTime() - timestamp = mediaFile.metadata.timeStamp(missing=None) - if timestamp is None: - timestamp = mediaFile.modificationTime - # if metadata has not been loaded, substitute other values - else: - timestamp = mediaFile.modificationTime - date = datetime.datetime.fromtimestamp(timestamp) + self.auto_start_is_on = ((not on_preference_change) and + ((self.prefs.auto_download_at_startup and + on_startup) or + (self.prefs.auto_download_upon_device_insertion and + not on_startup))) + - timestamp = int(timestamp) - - date_human_readable = date_time_human_readable(date) - name = mediaFile.name - size = mediaFile.size - thumbnail = mediaFile.thumbnail - - if mediaFile.genericThumbnail: - if mediaFile.isImage: - thumbnail_icon = self.generic_photo_thumbnail - else: - thumbnail_icon = self.generic_video_thumbnail - else: - thumbnail_icon = common.scale2pixbuf(60, 36, thumbnail) - - if DROP_SHADOW and not mediaFile.genericThumbnail: - pil_image = pixbuf_to_image(thumbnail_icon) - pil_image = self.iconDropShadow.dropShadow(pil_image) - thumbnail_icon = image_to_pixbuf(pil_image) + self.testing_auto_exit = False + self.testing_auto_exit_trip = len(mounts) + self.testing_auto_exit_trip_counter = 0 + + for m in mounts: + path, mount = m + device = dv.Device(path=path, mount=mount) + if (self.search_for_PSD() and + path not in self.prefs.device_whitelist): + # prompt user to see if device should be used or not + self.get_use_device(device) + else: + scan_pid = self.scan_manager.add_task(device) + if mount is not None: + self.mounts_by_path[path] = scan_pid + if not mounts: + self.set_download_action_sensitivity() - if mediaFile.isImage: - type_icon = self.icon_photo - else: - type_icon = self.icon_video - - status_icon = self.get_status_icon(mediaFile.status) - - if debug_info and False: - cmd_line('Thumbnail icon: %s' % thumbnail_icon) - cmd_line('Name: %s' % name) - cmd_line('Timestamp: %s' % timestamp) - cmd_line('Date: %s' % date_human_readable) - cmd_line('Size: %s %s' % (size, common.formatSizeForUser(size))) - cmd_line('Is an image: %s' % mediaFile.isImage) - cmd_line('Status: %s' % self.status_human_readable(mediaFile)) - cmd_line('Path: %s' % mediaFile.path) - cmd_line('Device name: %s' % mediaFile.deviceName) - cmd_line('Thread: %s' % mediaFile.thread_id) - cmd_line(' ') - - iter = self.liststore.append((thumbnail_icon, name, timestamp, date_human_readable, size, common.formatSizeForUser(size), mediaFile.isImage, type_icon, '', mediaFile, status_icon, mediaFile.status, mediaFile.path, mediaFile.deviceName, mediaFile.thread_id)) - - #create a reference to this row and store it in the mediaFile - path = self.liststore.get_path(iter) - mediaFile.treerowref = gtk.TreeRowReference(self.liststore, path) + def get_use_device(self, device): + """ Prompt user whether or not to download from this device """ - if mediaFile.status in [STATUS_CANNOT_DOWNLOAD, STATUS_WARNING]: - if not self.user_has_clicked_header: - self.liststore.set_sort_column_id(11, gtk.SORT_DESCENDING) + logger.info("Prompting whether to use %s", device.get_name()) + d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device) - def no_selected_rows_available_for_download(self): + def got_use_device(self, dialog, user_selected, permanent_choice, device): + """ User has chosen whether or not to use a device to download from """ + dialog.destroy() + + path = device.get_path() + + if user_selected: + if permanent_choice and path not in self.prefs.device_whitelist: + # do NOT do a list append operation here without the assignment, + # or the actual preferences will not be updated!! + if len(self.prefs.device_whitelist): + self.prefs.device_whitelist = self.prefs.device_whitelist + [path] + else: + self.prefs.device_whitelist = [path] + scan_pid = self.scan_manager.add_task(device) + self.mounts_by_path[path] = scan_pid + + elif permanent_choice and path not in self.prefs.device_blacklist: + # do not do a list append operation here without the assignment, or the preferences will not be updated! + if len(self.prefs.device_blacklist): + self.prefs.device_blacklist = self.prefs.device_blacklist + [path] + else: + self.prefs.device_blacklist = [path] + + def search_for_PSD(self): """ - Gets the number of rows the user has selected that can actually - be downloaded, and the threads they are found in + Check to see if user preferences are to automatically search for + Portable Storage Devices or not """ - v = 0 - threads = [] - model, paths = self.get_selection().get_selected_rows() - for path in paths: - iter = self.liststore.get_iter(path) - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - v += 1 - thread = self.get_thread(iter) - if thread not in threads: - threads.append(thread) - return v, threads - - def rows_available_for_download(self): + return self.prefs.device_autodetection_psd and self.prefs.device_autodetection + + def check_if_backup_mount(self, path): """ - Returns true if one or more rows has their status as STATUS_NOT_DOWNLOADED or STATUS_WARNING + Checks to see if backups are enabled and path represents a valid backup location + + Checks against user preferences. """ - iter = self.liststore.get_iter_first() - while iter: - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: + identifiers = [self.prefs.backup_identifier] + if DOWNLOAD_VIDEO: + identifiers.append(self.prefs.video_backup_identifier) + if self.prefs.backup_images: + if self.prefs.backup_device_autodetection: + if dv.is_backup_media(path, identifiers): + return True + elif path == self.prefs.backup_location: + # user manually specified the path return True - iter = self.liststore.iter_next(iter) - return False - - def update_download_selected_button(self): - """ - Updates the text on the Download Selection button, and set its sensitivity - """ - no_available_for_download = 0 - selection = self.get_selection() - model, paths = selection.get_selected_rows() - if paths: - path = paths[0] - iter = self.liststore.get_iter(path) - - #update button text - no_available_for_download, threads = self.no_selected_rows_available_for_download() + return False - if no_available_for_download and workers.scanComplete(threads): - self.rapidApp.download_selected_button.set_label(self.rapidApp.DOWNLOAD_SELECTED_LABEL + " (%s)" % no_available_for_download) - self.rapidApp.download_selected_button.set_sensitive(True) - else: - #nothing was selected, or nothing is available from what the user selected, or should not download right now - self.rapidApp.download_selected_button.set_label(self.rapidApp.DOWNLOAD_SELECTED_LABEL) - self.rapidApp.download_selected_button.set_sensitive(False) - - def on_selection_changed(self, selection): - """ - Update download selected button and preview the most recently - selected row in the treeview - """ - self.update_download_selected_button() - size = selection.count_selected_rows() - if size == 0: - self.selected_rows = set() - self.show_preview(None) - else: - if size <= len(self.selected_rows): - # discard everything, start over - self.selected_rows = set() - self.selection_size = size - model, paths = selection.get_selected_rows() - for path in paths: - iter = self.liststore.get_iter(path) - ref = self.get_mediaFile(iter).treerowref - - if ref not in self.selected_rows: - self.show_preview(iter) - self.selected_rows.add(ref) - - def clear_all(self, thread_id = None): - if thread_id is None: - self.liststore.clear() - self.show_preview(None) - else: - iter = self.liststore.get_iter_first() - while iter: - t = self.get_thread(iter) - if t == thread_id: - if self.previewed_file_treerowref: - mediaFile = self.get_mediaFile(iter) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(None) - self.liststore.remove(iter) - # need to start over, or else bad things happen - iter = self.liststore.get_iter_first() - else: - iter = self.liststore.iter_next(iter) - - def refreshSampleDownloadFolders(self, thread_id = None): + def using_volume_monitor(self): """ - Refreshes the download folder of every file that has not yet been downloaded - - This is useful when the user updates the preferences, and the scan has already occurred (or is occurring) - - If thread_id is specified, will only update rows with that thread + Returns True if programs needs to use gio volume monitor """ - for iter in self.get_tree_row_iters(): - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING, STATUS_CANNOT_DOWNLOAD]: - regenerate = True - if thread_id is not None: - t = self.get_thread(iter) - regenerate = t == thread_id - - if regenerate: - mediaFile = self.get_mediaFile(iter) - if mediaFile.isImage: - mediaFile.downloadFolder = self.rapidApp.prefs.download_folder - else: - mediaFile.downloadFolder = self.rapidApp.prefs.video_download_folder - mediaFile.samplePath = os.path.join(mediaFile.downloadFolder, mediaFile.sampleSubfolder) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def _refreshNameFactories(self): - sample_download_start_time = datetime.datetime.now() - self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.rapidApp.prefs.image_rename, self, - self.rapidApp.fileSequenceLock, sequences) - self.imageRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - self.videoRenamePrefsFactory = rn.VideoRenamePreferences(self.rapidApp.prefs.video_rename, self, - self.rapidApp.fileSequenceLock, sequences) - self.videoRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - self.subfolderPrefsFactory = rn.SubfolderPreferences(self.rapidApp.prefs.subfolder, self) - self.subfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - self.videoSubfolderPrefsFactory = rn.VideoSubfolderPreferences(self.rapidApp.prefs.video_subfolder, self) - self.videoSubfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - self.strip_characters = self.rapidApp.prefs.strip_characters - - def refreshGeneratedSampleSubfolderAndName(self, thread_id = None): + return (self.prefs.device_autodetection or + (self.prefs.backup_images and + self.prefs.backup_device_autodetection + )) + + def on_mount_added(self, vmonitor, mount): """ - Refreshes the name, subfolder and status of every file that has not yet been downloaded - - This is useful when the user updates the preferences, and the scan has already occurred (or is occurring) - - If thread_id is specified, will only update rows with that thread + callback run when gio indicates a new volume + has been mounted """ - self._setUsesJobCode() - self._refreshNameFactories() - for iter in self.get_tree_row_iters(): - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING, STATUS_CANNOT_DOWNLOAD]: - regenerate = True - if thread_id is not None: - t = self.get_thread(iter) - regenerate = t == thread_id - - if regenerate: - mediaFile = self.get_mediaFile(iter) - self.generateSampleSubfolderAndName(mediaFile, iter) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def generateSampleSubfolderAndName(self, mediaFile, iter): - problem = pn.Problem() - if mediaFile.isImage: - fallback_date = None - subfolderPrefsFactory = self.subfolderPrefsFactory - renamePrefsFactory = self.imageRenamePrefsFactory - nameUsesJobCode = self.imageRenameUsesJobCode - subfolderUsesJobCode = self.imageSubfolderUsesJobCode - else: - fallback_date = mediaFile.modificationTime - subfolderPrefsFactory = self.videoSubfolderPrefsFactory - renamePrefsFactory = self.videoRenamePrefsFactory - nameUsesJobCode = self.videoRenameUsesJobCode - subfolderUsesJobCode = self.videoSubfolderUsesJobCode - - renamePrefsFactory.setJobCode(self.get_job_code(iter)) - subfolderPrefsFactory.setJobCode(self.get_job_code(iter)) - - generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - self.strip_characters, fallback_date) - if self.get_status(iter) != mediaFile.status: - self.liststore.set(iter, 11, mediaFile.status) - self.liststore.set(iter, 10, self.get_status_icon(mediaFile.status)) - mediaFile.sampleStale = False - - def _setUsesJobCode(self): - self.imageRenameUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.image_rename) - self.imageSubfolderUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.subfolder) - self.videoRenameUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.video_rename) - self.videoSubfolderUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.video_subfolder) - - - def status_human_readable(self, mediaFile): - if mediaFile.status == STATUS_DOWNLOADED: - v = _('%(filetype)s was downloaded successfully') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_FAILED: - v = _('%(filetype)s was not downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOADED_WITH_WARNING: - v = _('%(filetype)s was downloaded with warnings') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_BACKUP_PROBLEM: - v = _('%(filetype)s was downloaded but there were problems backing up') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_AND_BACKUP_FAILED: - v = _('%(filetype)s was neither downloaded nor backed up') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_NOT_DOWNLOADED: - v = _('%(filetype)s is ready to be downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_PENDING: - v = _('%(filetype)s is about to be downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_WARNING: - v = _('%(filetype)s will be downloaded with warnings')% {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_CANNOT_DOWNLOAD: - v = _('%(filetype)s cannot be downloaded') % {'filetype': mediaFile.displayNameCap} - return v - - def show_preview(self, iter): - - if not iter: - # clear everything except the label Preview at the top - for widget in [self.parentApp.preview_original_name_label, - self.parentApp.preview_name_label, - self.parentApp.preview_status_label, - self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label]: - widget.set_text('') - - for widget in [self.parentApp.preview_image, - self.parentApp.preview_name_label, - self.parentApp.preview_original_name_label, - self.parentApp.preview_status_label, - self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label - ]: - widget.set_tooltip_text('') - - self.parentApp.preview_image.clear() - self.parentApp.preview_status_icon.clear() - self.parentApp.preview_destination_expander.hide() - self.parentApp.preview_device_expander.hide() - self.previewed_file_treerowref = None - - - elif not self.suspend_previews: - mediaFile = self.get_mediaFile(iter) - - self.previewed_file_treerowref = mediaFile.treerowref - - self.parentApp.set_base_preview_image(mediaFile.thumbnail) - thumbnail = self.parentApp.scaledPreviewImage() - - self.parentApp.preview_image.set_from_pixbuf(thumbnail) - - image_tool_tip = "%s\n%s" % (date_time_human_readable(mediaFile.dateTime(), False), common.formatSizeForUser(mediaFile.size)) - self.parentApp.preview_image.set_tooltip_text(image_tool_tip) - - if mediaFile.sampleStale and mediaFile.status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - self._refreshNameFactories() - self._setUsesJobCode() - self.generateSampleSubfolderAndName(mediaFile, iter) - - self.parentApp.preview_original_name_label.set_text(mediaFile.name) - self.parentApp.preview_original_name_label.set_tooltip_text(mediaFile.name) - if mediaFile.volume: - pixbuf = mediaFile.volume.get_icon_pixbuf(16) - else: - pixbuf = self.icontheme.load_icon('gtk-harddisk', 16, gtk.ICON_LOOKUP_USE_BUILTIN) - self.parentApp.preview_device_image.set_from_pixbuf(pixbuf) - self.parentApp.preview_device_label.set_text(mediaFile.deviceName) - self.parentApp.preview_device_path_label.set_text(mediaFile.path) - self.parentApp.preview_device_path_label.set_tooltip_text(mediaFile.path) - - if using_gio: - folder = gio.File(mediaFile.downloadFolder) - fileInfo = folder.query_info(gio.FILE_ATTRIBUTE_STANDARD_ICON) - icon = fileInfo.get_icon() - pixbuf = common.get_icon_pixbuf(using_gio, icon, 16, fallback='folder') - else: - pixbuf = self.icontheme.load_icon('folder', 16, gtk.ICON_LOOKUP_USE_BUILTIN) - - self.parentApp.preview_destination_image.set_from_pixbuf(pixbuf) - downloadFolderName = os.path.split(mediaFile.downloadFolder)[1] - self.parentApp.preview_destination_label.set_text(downloadFolderName) - - if mediaFile.status in [STATUS_WARNING, STATUS_CANNOT_DOWNLOAD, STATUS_NOT_DOWNLOADED, STATUS_DOWNLOAD_PENDING]: - - self.parentApp.preview_name_label.set_text(mediaFile.sampleName) - self.parentApp.preview_name_label.set_tooltip_text(mediaFile.sampleName) - self.parentApp.preview_destination_path_label.set_text(mediaFile.samplePath) - self.parentApp.preview_destination_path_label.set_tooltip_text(mediaFile.samplePath) - else: - self.parentApp.preview_name_label.set_text(mediaFile.downloadName) - self.parentApp.preview_name_label.set_tooltip_text(mediaFile.downloadName) - self.parentApp.preview_destination_path_label.set_text(mediaFile.downloadPath) - self.parentApp.preview_destination_path_label.set_tooltip_text(mediaFile.downloadPath) - - status_text = self.status_human_readable(mediaFile) - self.parentApp.preview_status_icon.set_from_pixbuf(self.get_status_icon(mediaFile.status, preview=True)) - self.parentApp.preview_status_label.set_markup('<b>' + status_text + '</b>') - self.parentApp.preview_status_label.set_tooltip_text(status_text) - - - if mediaFile.status in [STATUS_WARNING, STATUS_DOWNLOAD_FAILED, - STATUS_DOWNLOADED_WITH_WARNING, - STATUS_CANNOT_DOWNLOAD, - STATUS_BACKUP_PROBLEM, - STATUS_DOWNLOAD_AND_BACKUP_FAILED]: - problem_title = mediaFile.problem.get_title() - self.parentApp.preview_problem_title_label.set_markup('<i>' + problem_title + '</i>') - self.parentApp.preview_problem_title_label.set_tooltip_text(problem_title) - - problem_text = mediaFile.problem.get_problems() - self.parentApp.preview_problem_label.set_text(problem_text) - self.parentApp.preview_problem_label.set_tooltip_text(problem_text) - else: - self.parentApp.preview_problem_label.set_markup('') - self.parentApp.preview_problem_title_label.set_markup('') - for widget in [self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label - ]: - widget.set_tooltip_text('') - - if self.rapidApp.prefs.display_preview_folders: - self.parentApp.preview_destination_expander.show() - self.parentApp.preview_device_expander.show() - - - def select_rows(self, range): - selection = self.get_selection() - if range == 'all': - selection.select_all() - elif range == 'none': - selection.unselect_all() - else: - # User chose to select all photos or all videos, - # or select all files with or without job codes. - # Temporarily suspend previews while a large number of rows - # are being selected / unselected - self.suspend_previews = True - - iter = self.liststore.get_iter_first() - while iter is not None: - if range in ['photos', 'videos']: - type = self.get_is_image(iter) - select_row = (type and range == 'photos') or (not type and range == 'videos') - else: - job_code = self.get_job_code(iter) - select_row = (job_code and range == 'withjobcode') or (not job_code and range == 'withoutjobcode') - if select_row: - selection.select_iter(iter) - else: - selection.unselect_iter(iter) - iter = self.liststore.iter_next(iter) + if mount.is_shadowed(): + # ignore this type of mount + return - self.suspend_previews = False - # select the first photo / video - iter = self.liststore.get_iter_first() - while iter is not None: - type = self.get_is_image(iter) - if (type and range == 'photos') or (not type and range == 'videos'): - self.show_preview(iter) - break - iter = self.liststore.iter_next(iter) + path = mount.get_root().get_path() + if path is not None: + if path in self.prefs.device_blacklist and self.search_for_PSD(): + logger.info("Device %(device)s (%(path)s) ignored" % { + 'device': mount.get_name(), 'path': path}) + else: + is_backup_mount = self.check_if_backup_mount(path) + + if is_backup_mount: + if path not in self.backup_devices: + self.backup_devices[path] = mount + self.display_free_space() - def header_clicked(self, column): - self.user_has_clicked_header = True - - def display_filename_column(self, display): - """ - if display is true, the column will be shown - otherwise, it will not be shown + elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or + self.search_for_PSD()): + + self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion + device = dv.Device(path=path, mount=mount) + if self.search_for_PSD() and path not in self.prefs.device_whitelist: + # prompt user if device should be used or not + self.get_use_device(device) + else: + scan_pid = self.scan_manager.add_task(device) + self.mounts_by_path[path] = scan_pid + + def on_mount_removed(self, vmonitor, mount): + """ + callback run when gio indicates a new volume + has been mounted """ - self.filename_column.set_visible(display) - def display_size_column(self, display): - self.size_column.set_visible(display) + path = mount.get_root().get_path() - def display_type_column(self, display): - if not DOWNLOAD_VIDEO: - self.type_column.set_visible(False) - else: - self.type_column.set_visible(display) + # three scenarios - + # the mount has been scanned but downloading has not yet started + # files are being downloaded from mount (it must be a messy unmount) + # files have finished downloading from mount - def display_path_column(self, display): - self.path_column.set_visible(display) - - def display_device_column(self, display): - self.device_column.set_visible(display) - - def apply_job_code(self, job_code, overwrite=True, to_all_rows=False, thread_id=None): - """ - Applies the Job code to the selected rows, or all rows if to_all_rows is True. - - If overwrite is True, then it will overwrite any existing job code. - """ - - def _apply_job_code(): - status = self.get_status(iter) - if status in [STATUS_DOWNLOAD_PENDING, STATUS_WARNING, STATUS_NOT_DOWNLOADED]: + if path in self.mounts_by_path: + scan_pid = self.mounts_by_path[path] + del self.mounts_by_path[path] + # temp directory should be cleaned by finishing of process + + #~ if scan_pid in self.download_active_by_scan_pid: + #~ self._clean_temp_dirs_for_scan_pid(scan_pid) + self.thumbnails.clear_all(scan_pid = scan_pid, + keep_downloaded_files = True) + self.device_collection.remove_device(scan_pid) + - if mediaFile.isImage: - apply = rn.usesJobCode(self.rapidApp.prefs.image_rename) or rn.usesJobCode(self.rapidApp.prefs.subfolder) - else: - apply = rn.usesJobCode(self.rapidApp.prefs.video_rename) or rn.usesJobCode(self.rapidApp.prefs.video_subfolder) - if apply: - if overwrite: - self.liststore.set(iter, 8, job_code) - mediaFile.jobcode = job_code - mediaFile.sampleStale = True - else: - if not self.get_job_code(iter): - self.liststore.set(iter, 8, job_code) - mediaFile.jobcode = job_code - mediaFile.sampleStale = True - else: - pass - #if they got an existing job code, may as well keep it there in case the user - #reactivates job codes again in their prefs - - if to_all_rows or thread_id is not None: - for iter in self.get_tree_row_iters(): - apply = True - if thread_id is not None: - t = self.get_thread(iter) - apply = t == thread_id - if apply: - mediaFile = self.get_mediaFile(iter) - _apply_job_code() - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - else: - for iter in self.get_tree_row_iters(selected_only = True): - mediaFile = self.get_mediaFile(iter) - _apply_job_code() - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) + # remove backup volumes + elif path in self.backup_devices: + del self.backup_devices[path] + self.display_free_space() + + # may need to disable download button and menu + self.set_download_action_sensitivity() - def job_code_missing(self, selected_only): + def clear_non_running_downloads(self): """ - Returns True if any of the pending downloads do not have a - job code assigned. - - If selected_only is True, will only check in rows that the - user has selected. + Clears the display of downloads that are currently not running """ - def _job_code_missing(iter): - status = self.get_status(iter) - if status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - is_image = self.get_is_image(iter) - job_code = self.get_job_code(iter) - return needAJobCode.needAJobCode(job_code, is_image) - return False + # Stop any processes currently scanning or creating thumbnails + self._terminate_processes(terminate_file_copies=False) + + # Remove them from the user interface + for scan_pid in self.device_collection.get_all_displayed_processes(): + if scan_pid not in self.download_active_by_scan_pid: + self.device_collection.remove_device(scan_pid) + self.thumbnails.clear_all(scan_pid=scan_pid) + - self._setUsesJobCode() - needAJobCode = NeedAJobCode(self.rapidApp.prefs) - - v = False - if selected_only: - selection = self.get_selection() - model, pathlist = selection.get_selected_rows() - for path in pathlist: - iter = self.liststore.get_iter(path) - v = _job_code_missing(iter) - if v: - break - else: - iter = self.liststore.get_iter_first() - while iter: - v = _job_code_missing(iter) - if v: - break - iter = self.liststore.iter_next(iter) - return v - def _set_download_pending(self, iter, threads): - existing_status = self.get_status(iter) - if existing_status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - self.liststore.set(iter, 11, STATUS_DOWNLOAD_PENDING) - self.liststore.set(iter, 10, self.download_pending_icon) - # this value is in a thread's list of files to download - mediaFile = self.get_mediaFile(iter) - # each thread will see this change in status - mediaFile.status = STATUS_DOWNLOAD_PENDING - thread = self.get_thread(iter) - if thread not in threads: - threads.append(thread) - - def set_status_to_download_pending(self, selected_only, thread_id=None): + # # # + # Download and help buttons, and menu items + # # # + + def on_download_action_activate(self, action): """ - Sets status of files to be download pending, if they are waiting to be downloaded - if selected_only is true, only applies to selected rows - - If thread_id is not None, then after the statuses have been set, - the thread will be restarted (this is intended for the cases - where this method is called from a thread and auto start is True) - - Returns a list of threads which can be downloaded + Called when a download is activated """ - threads = [] - if selected_only: - for iter in self.get_tree_row_iters(selected_only = True): - self._set_download_pending(iter, threads) + if self.copy_files_manager.paused: + logger.debug("Download resumed") + self.resume_download() else: - for iter in self.get_tree_row_iters(): - apply = True - if thread_id is not None: - t = self.get_thread(iter) - apply = t == thread_id - if apply: - self._set_download_pending(iter, threads) - - if thread_id is not None: - # restart the thread - workers[thread_id].startStop() - return threads - - def update_status_post_download(self, treerowref): - path = treerowref.get_path() - if not path: - sys.stderr.write("FIXME: SelectionTreeView treerowref no longer refers to valid row\n") - else: - iter = self.liststore.get_iter(path) - mediaFile = self.get_mediaFile(iter) - status = mediaFile.status - self.liststore.set(iter, 11, status) - self.liststore.set(iter, 10, self.get_status_icon(status)) + logger.debug("Download activated") - # If this row is currently previewed, then should update the preview - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - -class SelectionVBox(gtk.VBox): - """ - Dialog from which the user can select photos and videos to download - """ + if self.download_action_is_download: + if self.need_job_code_for_naming and not self.prompting_for_job_code: + self.get_job_code() + else: + self.start_download() + else: + self.pause_download() - def __init__(self, parentApp): - """ - Initialize values for log dialog, but do not display. - """ - - gtk.VBox.__init__(self) - self.parentApp = parentApp - - tiny_screen = TINY_SCREEN - if tiny_screen: - config.max_thumbnail_size = 160 + def on_help_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/documentation") - selection_scrolledwindow = gtk.ScrolledWindow() - selection_scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - selection_viewport = gtk.Viewport() - - - self.selection_treeview = SelectionTreeView(self) - - selection_scrolledwindow.add(self.selection_treeview) + def on_preferences_action_activate(self, action): - - # Job code controls - self.add_job_code_combo() - left_pane_vbox = gtk.VBox(spacing = 12) - left_pane_vbox.pack_start(selection_scrolledwindow, True, True) - left_pane_vbox.pack_start(self.job_code_hbox, False, True) - - # Window sizes - #selection_scrolledwindow.set_size_request(350, -1) - + preferencesdialog.PreferencesDialog(self) - # Preview pane - - # Zoom in and out slider (make the image bigger / smaller) - - # Zoom out (on the left of the slider) - self.zoom_out_eventbox = gtk.EventBox() - self.zoom_out_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK) - self.zoom_out_image = gtk.Image() - self.zoom_out_image.set_from_file(paths.share_dir('glade3/zoom-out.png')) - self.zoom_out_eventbox.add(self.zoom_out_image) - self.zoom_out_eventbox.connect("button_press_event", self.zoom_out_0_callback) - - # Zoom in (on the right of the slider) - self.zoom_in_eventbox = gtk.EventBox() - self.zoom_in_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK) - self.zoom_in_image = gtk.Image() - self.zoom_in_image.set_from_file(paths.share_dir('glade3/zoom-in.png')) - self.zoom_in_eventbox.add(self.zoom_in_image) - self.zoom_in_eventbox.connect("button_press_event", self.zoom_in_100_callback) - - self.slider_adjustment = gtk.Adjustment(value=self.parentApp.prefs.preview_zoom, - lower=config.MIN_THUMBNAIL_SIZE, upper=config.max_thumbnail_size, - step_incr=1.0, page_incr=config.THUMBNAIL_INCREMENT, page_size=0) - self.slider_adjustment.connect("value_changed", self.resize_image_callback) - self.slider_hscale = gtk.HScale(self.slider_adjustment) - self.slider_hscale.set_draw_value(False) # don't display numeric value - self.slider_hscale.set_size_request(config.MIN_THUMBNAIL_SIZE * 2, -1) + def set_download_action_sensitivity(self): + """ + Sets sensitivity of Download action to enable or disable it + Affects download button and menu item + """ + if not self.download_is_occurring(): + sensitivity = False + if self.scan_manager.no_tasks == 0: + if self.thumbnails.files_are_checked_to_download(): + sensitivity = True + + self.download_action.set_sensitive(sensitivity) + + def set_download_action_label(self, is_download): + """ + Toggles label betwen pause and download + """ - #Preview image - self.base_preview_image = None # large size image used to scale down from - self.preview_image = gtk.Image() - - self.preview_image.set_alignment(0, 0.5) - #leave room for thumbnail shadow - if DROP_SHADOW: - self.cacheDropShadow() + if is_download: + self.download_action.set_label(_("Download")) + self.download_action_is_download = True else: - self.shadow_size = 0 - - image_size, shadow_size, offset = self._imageAndShadowSize() - - self.preview_image.set_size_request(image_size, image_size) - - #labels to display file information - - #Original filename - self.preview_original_name_label = gtk.Label() - self.preview_original_name_label.set_alignment(0, 0.5) - self.preview_original_name_label.set_ellipsize(pango.ELLIPSIZE_END) - - - #Device (where it will be downloaded from) - self.preview_device_expander = gtk.Expander() - self.preview_device_label = gtk.Label() - self.preview_device_label.set_alignment(0, 0.5) - self.preview_device_image = gtk.Image() - - self.preview_device_path_label = gtk.Label() - self.preview_device_path_label.set_alignment(0, 0.5) - self.preview_device_path_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) - self.preview_device_path_label.set_padding(30, 0) - self.preview_device_expander.add(self.preview_device_path_label) - - device_hbox = gtk.HBox(False, spacing = 6) - device_hbox.pack_start(self.preview_device_image) - device_hbox.pack_start(self.preview_device_label, True, True) - - self.preview_device_expander.set_label_widget(device_hbox) - - #Filename that has been generated - self.preview_name_label = gtk.Label() - self.preview_name_label.set_alignment(0, 0.5) - self.preview_name_label.set_ellipsize(pango.ELLIPSIZE_END) - - #Download destination - self.preview_destination_expander = gtk.Expander() - self.preview_destination_label = gtk.Label() - self.preview_destination_label.set_alignment(0, 0.5) - self.preview_destination_image = gtk.Image() + self.download_action.set_label(_("Pause")) + self.download_action_is_download = False + + # # # + # Job codes + # # # + + + def _init_job_code(self): + self.job_code = self.last_chosen_job_code = '' + self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code() + self.prompting_for_job_code = False + + def assign_job_code(self, code): + """ assign job code (which may be empty) to member variable and update user preferences - self.preview_destination_path_label = gtk.Label() - self.preview_destination_path_label.set_alignment(0, 0.5) - self.preview_destination_path_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) - self.preview_destination_path_label.set_padding(30, 0) - self.preview_destination_expander.add(self.preview_destination_path_label) + Update preferences only if code is not empty. Do not duplicate job code. + """ - destination_hbox = gtk.HBox(False, spacing = 6) - destination_hbox.pack_start(self.preview_destination_image) - destination_hbox.pack_start(self.preview_destination_label, True, True) + self.job_code = code - self.preview_destination_expander.set_label_widget(destination_hbox) + if code: + #add this value to job codes preferences + #delete any existing value which is the same + #(this way it comes to the front, which is where it should be) + #never modify self.prefs.job_codes in place! (or prefs become screwed up) + + jcs = self.prefs.job_codes + while code in jcs: + jcs.remove(code) + + self.prefs.job_codes = [code] + jcs + def _get_job_code(self, post_job_code_entry_callback): + """ prompt for a job code """ - #Status of the file + if not self.prompting_for_job_code: + logger.debug("Prompting for Job Code") + self.prompting_for_job_code = True + j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp, + job_codes = self.prefs.job_codes, + default_job_code = self.last_chosen_job_code, + post_job_code_entry_callback=post_job_code_entry_callback, + entry_only = False) + else: + logger.debug("Already prompting for Job Code, do not prompt again") - self.preview_status_icon = gtk.Image() - self.preview_status_icon.set_size_request(16,16) - - self.preview_status_label = gtk.Label() - self.preview_status_label.set_alignment(0, 0.5) - self.preview_status_label.set_ellipsize(pango.ELLIPSIZE_END) - self.preview_status_label.set_padding(12, 0) - - #Title of problems encountered in generating the name / subfolder - self.preview_problem_title_label = gtk.Label() - self.preview_problem_title_label.set_alignment(0, 0.5) - self.preview_problem_title_label.set_ellipsize(pango.ELLIPSIZE_END) - self.preview_problem_title_label.set_padding(12, 0) + def get_job_code(self): + self._get_job_code(self.got_job_code) + + def got_job_code(self, dialog, user_chose_code, code): + dialog.destroy() + self.prompting_for_job_code = False - #Details of what the problem(s) are - self.preview_problem_label = gtk.Label() - self.preview_problem_label.set_alignment(0, 0) - self.preview_problem_label.set_line_wrap(True) - self.preview_problem_label.set_padding(12, 0) - #Can't combine wrapping and ellipsize, sadly - #self.preview_problem_label.set_ellipsize(pango.ELLIPSIZE_END) + if user_chose_code: + if code is None: + code = '' + self.assign_job_code(code) + self.last_chosen_job_code = code + logger.debug("Job Code %s entered", self.job_code) + self.start_download() - #Put content into table - # Use a table so we can do the Gnome HIG layout more easily - self.preview_table = gtk.Table(10, 4) - self.preview_table.set_row_spacings(12) - left_spacer = gtk.Label('') - left_spacer.set_padding(12, 0) - right_spacer = gtk.Label('') - right_spacer.set_padding(6, 0) - - - spacer2 = gtk.Label('') - - #left and right spacers - self.preview_table.attach(left_spacer, 0, 1, 1, 2, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - self.preview_table.attach(right_spacer, 3, 4, 1, 2, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - - row = 0 - zoom_hbox = gtk.HBox() - zoom_hbox.pack_start(self.zoom_out_eventbox, False, False) - zoom_hbox.pack_start(self.slider_hscale, False, False) - zoom_hbox.pack_start(self.zoom_in_eventbox, False, False) - - self.preview_table.attach(zoom_hbox, 1, 3, row, row+1, yoptions=gtk.SHRINK) - - row += 1 - self.preview_table.attach(self.preview_image, 1, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_original_name_label, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - if not tiny_screen: - self.preview_table.attach(self.preview_device_expander, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_name_label, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - if not tiny_screen: - self.preview_table.attach(self.preview_destination_expander, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - - if not tiny_screen: - self.preview_table.attach(spacer2, 0, 7, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_status_icon, 1, 2, row, row+1, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - self.preview_table.attach(self.preview_status_label, 2, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_problem_title_label, 2, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - self.preview_table.attach(self.preview_problem_label, 2, 4, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.EXPAND|gtk.FILL) - row += 1 - - self.file_hpaned = gtk.HPaned() - self.file_hpaned.pack1(left_pane_vbox, shrink=False) - self.file_hpaned.pack2(self.preview_table, resize=True, shrink=False) - self.pack_start(self.file_hpaned, True, True) - if self.parentApp.prefs.hpaned_pos > 0: - self.file_hpaned.set_position(self.parentApp.prefs.hpaned_pos) else: - # this is what the user will see the first time they run the app - self.file_hpaned.set_position(300) - - self.show_all() + # user cancelled + logger.debug("No Job Code entered") + self.job_code = '' + self.auto_start_is_on = False + + # # # + # Download + # # # - def set_base_preview_image(self, pixbuf): + def _init_download_tracking(self): """ - sets the unscaled pixbuf image to be displayed to the user - the actual image the user will see will depend on the scale - they've set to view it at + Initialize variables to track downloads """ - self.base_preview_image = pixbuf + # Track download sizes and other values for each device. + # (Scan id acts as an index to each device. A device could be scanned + # more than once). + self.download_tracker = downloadtracker.DownloadTracker() - def zoom_in(self): - self.slider_adjustment.set_value(min([config.max_thumbnail_size, int(self.slider_adjustment.get_value()) + config.THUMBNAIL_INCREMENT])) + # Track which temporary directories are created when downloading files + self.temp_dirs_by_scan_pid = dict() - def zoom_out(self): - self.slider_adjustment.set_value(max([config.MIN_THUMBNAIL_SIZE, int(self.slider_adjustment.get_value()) - config.THUMBNAIL_INCREMENT])) - - def zoom_in_100_callback(self, widget, value): - self.slider_adjustment.set_value(config.max_thumbnail_size) + # Track which downloads are running + self.download_active_by_scan_pid = [] - def zoom_out_0_callback(self, widget, value): - self.slider_adjustment.set_value(config.MIN_THUMBNAIL_SIZE) - - def set_display_preview_folders(self, value): - if value and self.selection_treeview.previewed_file_treerowref: - self.preview_destination_expander.show() - self.preview_device_expander.show() - else: - self.preview_destination_expander.hide() - self.preview_device_expander.hide() - - def cacheDropShadow(self): - i, self.shadow_size, offset_v = self._imageAndShadowSize() - self.drop_shadow = DropShadow(offset=(offset_v,offset_v), shadow = (0x44, 0x44, 0x44, 0xff), border=self.shadow_size, trim_border = True) - - def _imageAndShadowSize(self): - image_size = int(self.slider_adjustment.get_value()) - offset_v = max([image_size / 25, 5]) # realistically size the shadow based on the size of the image - shadow_size = offset_v + 3 - image_size = image_size + offset_v * 2 + 3 - return (image_size, shadow_size, offset_v) - - def resize_image_callback(self, adjustment): - """ - Resize the preview image after the adjustment value has been - changed - """ - size = int(adjustment.value) - self.parentApp.prefs.preview_zoom = size - self.cacheDropShadow() - - pixbuf = self.scaledPreviewImage() - if pixbuf: - self.preview_image.set_from_pixbuf(pixbuf) - size = max([pixbuf.get_width(), pixbuf.get_height()]) - self.preview_image.set_size_request(size, size) - else: - self.preview_image.set_size_request(size + self.shadow_size, size + self.shadow_size) - - def scaledPreviewImage(self): - """ - Generate a scaled version of the preview image - """ - size = int(self.slider_adjustment.get_value()) - if not self.base_preview_image: - return None - else: - pixbuf = common.scale2pixbuf(size, size, self.base_preview_image) - - if DROP_SHADOW: - pil_image = pixbuf_to_image(pixbuf) - pil_image = self.drop_shadow.dropShadow(pil_image) - pixbuf = image_to_pixbuf(pil_image) - - return pixbuf - def set_job_code_display(self): + def start_download(self, scan_pid=None): """ - Shows or hides the job code entry + Start download, renaming and backup of files. - If user is not using job codes in their file or subfolder names - then do not prompt for it + If scan_pid is specified, only files matching it will be downloaded """ - - if self.parentApp.needJobCodeForRenaming(): - self.job_code_hbox.show() - self.job_code_label.show() - self.job_code_combo.show() - self.selection_treeview.job_code_column.set_visible(True) - else: - self.job_code_hbox.hide() - self.job_code_label.hide() - self.job_code_combo.hide() - self.selection_treeview.job_code_column.set_visible(False) - - def update_job_code_combo(self): - # delete existing rows - while len(self.job_code_combo.get_model()) > 0: - self.job_code_combo.remove_text(0) - # add new ones - for text in self.parentApp.prefs.job_codes: - self.job_code_combo.append_text(text) - # clear existing entry displayed in entry box - self.job_code_entry.set_text('') - - def add_job_code_combo(self): - self.job_code_hbox = gtk.HBox(spacing = 12) - self.job_code_hbox.set_no_show_all(True) - self.job_code_label = gtk.Label(_("Job Code:")) - - self.job_code_combo = gtk.combo_box_entry_new_text() - for text in self.parentApp.prefs.job_codes: - self.job_code_combo.append_text(text) - - # make entry box have entry completion - self.job_code_entry = self.job_code_combo.child - - self.completion = gtk.EntryCompletion() - self.completion.set_match_func(self.job_code_match_func) - self.completion.connect("match-selected", - self.on_job_code_combo_completion_match) - self.completion.set_model(self.job_code_combo.get_model()) - self.completion.set_text_column(0) - self.job_code_entry.set_completion(self.completion) - - - self.job_code_combo.connect('changed', self.on_job_code_resp) - - self.job_code_entry.connect('activate', self.on_job_code_entry_resp) - - self.job_code_combo.set_tooltip_text(_("Enter a new Job Code and press Enter, or select an existing Job Code")) - - #add widgets - self.job_code_hbox.pack_start(self.job_code_label, False, False) - self.job_code_hbox.pack_start(self.job_code_combo, True, True) - self.set_job_code_display() - - def job_code_match_func(self, completion, key, iter): - model = completion.get_model() - return model[iter][0].lower().startswith(self.job_code_entry.get_text().lower()) - - def on_job_code_combo_completion_match(self, completion, model, iter): - self.job_code_entry.set_text(model[iter][0]) - self.job_code_entry.set_position(-1) - - def on_job_code_resp(self, widget): - """ - When the user has clicked on an existing job code - """ + files_by_scan_pid = self.thumbnails.get_files_checked_for_download(scan_pid) + folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid) - # ignore changes because the user is typing in a new value - if widget.get_active() >= 0: - self.job_code_chosen(widget.get_active_text()) + if not folders_valid: + if len(invalid_dirs) > 1: + msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % { + 'folder1': invalid_dirs[0], 'folder2': invalid_dirs[1]} + else: + msg = _("This download folder is invalid:\n%s") % invalid_dirs[0] + self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"), + msg) + else: + # set time download is starting if it is not already set + # it is unset when all downloads are completed + if self.download_start_time is None: + self.download_start_time = datetime.datetime.now() - def on_job_code_entry_resp(self, widget): - """ - When the user has hit enter after entering a new job code - """ - self.job_code_chosen(widget.get_text()) + self.thumbnails.mark_download_pending(files_by_scan_pid) + for scan_pid in files_by_scan_pid: + files = files_by_scan_pid[scan_pid] + self.download_files(files, scan_pid) + + self.set_download_action_label(is_download = False) - def job_code_chosen(self, job_code): - """ - The user has selected a Job code, apply it to selected images. - """ - self.selection_treeview.apply_job_code(job_code, overwrite = True) - self.completion.set_model(None) - self.parentApp.assignJobCode(job_code) - self.completion.set_model(self.job_code_combo.get_model()) + def pause_download(self): + + self.copy_files_manager.pause() + + # set action to display Download + if not self.download_action_is_download: + self.set_download_action_label(is_download = True) - def add_file(self, mediaFile): - self.selection_treeview.add_file(mediaFile) + self.time_check.pause() + def resume_download(self): + for scan_pid in self.download_active_by_scan_pid: + self.time_remaining.set_time_mark(scan_pid) -class LogDialog(gnomeglade.Component): - """ - Displays a log of errors, warnings or other information to the user - """ - - def __init__(self, parentApp): + self.time_check.set_download_mark() + + self.copy_files_manager.start() + + def download_files(self, files, scan_pid): """ - Initialize values for log dialog, but do not display. + Initiate downloading and renaming of files """ - gnomeglade.Component.__init__(self, - paths.share_dir(config.GLADE_FILE), - "logdialog") - - - self.widget.connect("delete-event", self.hide_window) + # Check which file types will be downloaded for this particular process + if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + photo_download_folder = self.prefs.download_folder + else: + photo_download_folder = None + + if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): + video_download_folder = self.prefs.video_download_folder + else: + video_download_folder = None - self.parentApp = parentApp - self.log_textview.set_cursor_visible(False) - self.textbuffer = self.log_textview.get_buffer() + download_size = self.size_files_to_be_downloaded(files) + self.download_tracker.init_stats(scan_pid=scan_pid, + bytes=download_size, + no_files=len(files)) - self.errorTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD, foreground="red") - self.warningTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) - self.resolutionTag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) + self.time_remaining.set(scan_pid, download_size) + self.time_check.set_download_mark() + + self.download_active_by_scan_pid.append(scan_pid) - def addMessage(self, thread_id, severity, problem, details, resolution): - if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: - self.parentApp.error_image.show() - elif severity == config.WARNING: - self.parentApp.warning_image.show() - self.parentApp.warning_vseparator.show() - iter = self.textbuffer.get_end_iter() - if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: - self.textbuffer.insert_with_tags(iter, problem +"\n", self.errorTag) - else: - self.textbuffer.insert_with_tags(iter, problem +"\n", self.warningTag) - if details: - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert(iter, details + "\n") - if resolution: - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert_with_tags(iter, resolution +"\n", self.resolutionTag) + if len(self.download_active_by_scan_pid) > 1: + self.display_summary_notification = True - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert(iter, "\n") - - # move viewport to display the latest message - adjustment = self.log_scrolledwindow.get_vadjustment() - adjustment.set_value(adjustment.upper) - - - def on_logdialog_response(self, dialog, arg): - if arg == gtk.RESPONSE_CLOSE: - pass - self.parentApp.error_image.hide() - self.parentApp.warning_image.hide() - self.parentApp.warning_vseparator.hide() - self.parentApp.prefs.show_log_dialog = False - self.widget.hide() - return True - - def hide_window(self, window, event): - window.hide() - return True - + # Initiate copy files process + self.copy_files_manager.add_task((photo_download_folder, + video_download_folder, scan_pid, + files, self.auto_start_is_on)) + + def copy_files_results(self, source, condition): + """ + Handle results from copy files process + """ + #FIXME: must handle early termination / pause of copy files process + connection = self.copy_files_manager.get_pipe(source) + conn_type, msg_data = connection.recv() + if conn_type == rpdmp.CONN_PARTIAL: + msg_type, data = msg_data + if msg_type == rpdmp.MSG_TEMP_DIRS: + scan_pid, photo_temp_dir, video_temp_dir = data + self.temp_dirs_by_scan_pid[scan_pid] = (photo_temp_dir, video_temp_dir) + elif msg_type == rpdmp.MSG_BYTES: + scan_pid, total_downloaded, chunk_downloaded = data + self.download_tracker.set_total_bytes_copied(scan_pid, + total_downloaded) + self.time_check.increment(bytes_downloaded=chunk_downloaded) + percent_complete = self.download_tracker.get_percent_complete(scan_pid) + self.device_collection.update_progress(scan_pid, percent_complete, + None, None) + self.time_remaining.update(scan_pid, total_downloaded) + elif msg_type == rpdmp.MSG_FILE: + download_succeeded, rpd_file, download_count, temp_full_file_name = data + + self.download_tracker.set_download_count_for_file( + rpd_file.unique_id, download_count) + self.download_tracker.set_download_count( + rpd_file.scan_pid, download_count) + rpd_file.download_start_time = self.download_start_time + + if download_succeeded: + # Insert preference values needed for name generation + rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) + rpd_file.strip_characters = self.prefs.strip_characters + rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) + rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution + rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() + rpd_file.job_code = self.job_code + + self.subfolder_file_manager.rename_file_and_move_to_subfolder( + download_succeeded, + download_count, + rpd_file + ) + elif msg_type == rpdmp.MSG_THUMB: + #~ unique_id, thumbnail, thumbnail_icon = data + #~ thumbnail_data = (unique_id + self.thumbnails.update_thumbnail(data) + + return True + else: + # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE + connection.close() + return False + -class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): - def __init__(self, bus, path, name): + + def download_is_occurring(self): + """Returns True if a file is currently being downloaded or renamed + """ + v = not len(self.download_active_by_scan_pid) == 0 + #~ logger.info("Download is occurring: %s", v) + return v + + # # # + # Create folder and file names for downloaded files + # # # + + def subfolder_file_results(self, move_succeeded, rpd_file): + """ + Handle results of subfolder creation and file renaming + """ + + scan_pid = rpd_file.scan_pid + unique_id = rpd_file.unique_id - dbus.service.Object.__init__ (self, bus, path, name) - self.running = False + self.thumbnails.update_status_post_download(rpd_file) - gladefile = paths.share_dir(config.GLADE_FILE) - - gnomeglade.GnomeApp.__init__(self, "rapid", __version__, gladefile, "rapidapp") - - # notifications - self.displayDownloadSummaryNotification = False - self.initPyNotify() + # Update error log window if neccessary + if not move_succeeded: + self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + elif rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: + self.log_error(config.WARNING, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) - self.prefs = RapidPreferences() - self.prefs.notify_add(self.on_preference_changed) + self.download_tracker.file_downloaded_increment(scan_pid, + rpd_file.file_type, + rpd_file.status) + + completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id) - self.testing = False - if self.testing: - self.setTestingEnv() + if self.download_is_occurring(): + self.update_time_remaining() + + if completed: + # Last file for this scan pid has been downloaded, so clean temp directory + logger.debug("Purging temp directories") + self._clean_temp_dirs_for_scan_pid(scan_pid) + self.download_active_by_scan_pid.remove(scan_pid) + self.time_remaining.remove(scan_pid) + self.notify_downloaded_from_device(scan_pid) + if files_remaining == 0 and self.prefs.auto_unmount: + self.device_collection.unmount(scan_pid) -# sys.exit(0) + + if not self.download_is_occurring(): + logger.debug("Download completed") + self.notify_download_complete() + self.download_progressbar.set_fraction(0.0) + + self.prefs.stored_sequence_no = self.stored_sequence_value.value + self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today_value.value) + self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date_value.value) + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - # remember the window size from the last time the program was run - if self.prefs.main_window_maximized: - self.rapidapp.maximize() - elif self.prefs.main_window_size_x > 0: - self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y) - else: - # set a default size - self.rapidapp.set_default_size(650, 650) + if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings()) + or self.prefs.auto_exit_force): + if not self.thumbnails.files_remain_to_download(): + gtk.main_quit() + + self.download_tracker.purge_all() + self.speed_label.set_label(" ") + + self.display_free_space() + + self.set_download_action_label(is_download=True) + self.set_download_action_sensitivity() + + self.job_code = '' + self.download_start_time = None + + + def update_time_remaining(self): + update, download_speed = self.time_check.check_for_update() + if update: + self.speed_label.set_text(download_speed) - if gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT: - self.prefs.display_preview_folders = False - self.menu_preview_folders.set_sensitive(False) + time_remaining = self.time_remaining.time_remaining() + if time_remaining: + secs = int(time_remaining) - self.widget.show() - - self._setupIcons() - - # this must come after the window is shown - if self.prefs.vpaned_pos > 0: - self.main_vpaned.set_position(self.prefs.vpaned_pos) + if secs == 0: + message = "" + elif secs == 1: + message = _("About 1 second remaining") + elif secs < 60: + message = _("About %i seconds remaining") % secs + elif secs == 60: + message = _("About 1 minute remaining") + else: + # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. + # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount + # of time the download has remainging, e.g. 'About 5:36 minutes remaining' + message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} + + self.rapid_statusbar.pop(self.statusbar_context_id) + self.rapid_statusbar.push(self.statusbar_context_id, message) + + def file_types_by_number(self, no_photos, no_videos): + """ + returns a string to be displayed to the user that can be used + to show if a value refers to photos or videos or both, or just one + of each + """ + if (no_videos > 0) and (no_photos > 0): + v = _('photos and videos') + elif (no_videos == 0) and (no_photos == 0): + v = _('photos or videos') + elif no_videos > 0: + if no_videos > 1: + v = _('videos') + else: + v = _('video') else: - self.main_vpaned.set_position(66) - - self.checkIfFirstTimeProgramEverRun() + if no_photos > 1: + v = _('photos') + else: + v = _('photo') + return v - displayPreferences = self.checkForUpgrade(__version__) - self.prefs.program_version = __version__ - - self.timeRemaining = TimeRemaining() - self._resetDownloadInfo() - self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") - - # hide display of warning and error symbols in the taskbar until they are needed - self.error_image.hide() - self.warning_image.hide() - self.warning_vseparator.hide() + def notify_downloaded_from_device(self, scan_pid): + device = self.device_collection.get_device(scan_pid) + + if device.mount is None: + notificationName = PROGRAM_NAME + else: + notificationName = device.get_name() + + no_photos_downloaded = self.download_tracker.get_no_files_downloaded( + scan_pid, rpdfile.FILE_TYPE_PHOTO) + no_videos_downloaded = self.download_tracker.get_no_files_downloaded( + scan_pid, rpdfile.FILE_TYPE_VIDEO) + no_photos_failed = self.download_tracker.get_no_files_failed( + scan_pid, rpdfile.FILE_TYPE_PHOTO) + no_videos_failed = self.download_tracker.get_no_files_failed( + scan_pid, rpdfile.FILE_TYPE_VIDEO) + no_files_downloaded = no_photos_downloaded + no_videos_downloaded + no_files_failed = no_photos_failed + no_videos_failed + no_warnings = self.download_tracker.get_no_warnings(scan_pid) + + file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded) + file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed) + message = _("%(noFiles)s %(filetypes)s downloaded") % \ + {'noFiles':no_files_downloaded, 'filetypes': file_types} + + if no_files_failed: + message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed} + + if no_warnings: + message = "%s\n%s " % (message, no_warnings) + _("warnings") + + n = pynotify.Notification(notificationName, message) + n.set_icon_from_pixbuf(device.get_icon(self.notification_icon_size)) + + n.show() + + def notify_download_complete(self): + if self.display_summary_notification: + message = _("All downloads complete") + + # photo downloads + photo_downloads = self.download_tracker.total_photos_downloaded + if photo_downloads: + filetype = self.file_types_by_number(photo_downloads, 0) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': photo_downloads, + 'numberdownloaded': _("%(filetype)s downloaded") % \ + {'filetype': filetype}} + + # photo failures + photo_failures = self.download_tracker.total_photo_failures + if photo_failures: + filetype = self.file_types_by_number(photo_failures, 0) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': photo_failures, + 'numberdownloaded': _("%(filetype)s failed to download") % \ + {'filetype': filetype}} + + # video downloads + video_downloads = self.download_tracker.total_videos_downloaded + if video_downloads: + filetype = self.file_types_by_number(0, video_downloads) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': video_downloads, + 'numberdownloaded': _("%(filetype)s downloaded") % \ + {'filetype': filetype}} + + # video failures + video_failures = self.download_tracker.total_video_failures + if video_failures: + filetype = self.file_types_by_number(0, video_failures) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': video_failures, + 'numberdownloaded': _("%(filetype)s failed to download") % \ + {'filetype': filetype}} + + # warnings + warnings = self.download_tracker.total_warnings + if warnings: + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': warnings, + 'numberdownloaded': _("warnings")} + + n = pynotify.Notification(PROGRAM_NAME, message) + n.set_icon_from_pixbuf(self.application_icon) + n.show() + self.display_summary_notification = False # don't show it again unless needed + - if not displayPreferences: - displayPreferences = not self.checkPreferencesOnStartup() + def _update_file_download_device_progress(self, scan_pid, unique_id): + """ + Increments the progress bar for an individual device - # display download information using threads - global media_collection_treeview, log_dialog - global workers - - #track files that should have a suffix added to them - global duplicate_files + Returns if the download is completed for that scan_pid + It also returns the number of files remaining for the scan_pid, BUT + this value is valid ONLY if the download is completed + """ - #track files that have been downloaded in this session - global downloaded_files + files_downloaded = self.download_tracker.get_download_count_for_file(unique_id) + files_to_download = self.download_tracker.get_no_files_in_download(scan_pid) + file_types = self.download_tracker.get_file_types_present(scan_pid) + completed = files_downloaded == files_to_download - # control sequence numbers and letters - global sequences + if completed: + files_remaining = self.thumbnails.get_no_files_remaining(scan_pid) + else: + files_remaining = 0 + + if completed and files_remaining: + # e.g.: 3 of 205 photos and videos (202 remaining) + progress_bar_text = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % { + 'number': files_downloaded, + 'total': files_to_download, + 'filetypes': file_types, + 'remaining': files_remaining} + else: + # e.g.: 205 of 205 photos and videos + progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \ + {'number': files_downloaded, + 'total': files_to_download, + 'filetypes': file_types} + percent_complete = self.download_tracker.get_percent_complete(scan_pid) + self.device_collection.update_progress(scan_pid=scan_pid, + percent_complete=percent_complete, + progress_bar_text=progress_bar_text, + bytes_downloaded=None) + + percent_complete = self.download_tracker.get_overall_percent_complete() + self.download_progressbar.set_fraction(percent_complete) + + return (completed, files_remaining) - # whether we need to prompt for a job code - global need_job_code_for_renaming - duplicate_files = {} - downloaded_files = DownloadedFiles() + def _clean_all_temp_dirs(self): + """ + Cleans all temp dirs if they exist + """ + for scan_pid in self.temp_dirs_by_scan_pid: + for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: + self._purge_dir(temp_dir) + + self.temp_dirs_by_scan_pid = {} + + + def _clean_temp_dirs_for_scan_pid(self, scan_pid): + """ + Deletes temp files and folders used in download + """ + for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: + self._purge_dir(temp_dir) + del self.temp_dirs_by_scan_pid[scan_pid] + + def _purge_dir(self, directory): + """ + Deletes all files in the directory, and the directory itself. - self.download_start_time = None + Does not recursively traverse any subfolders in the directory. + """ - downloadsToday = self.prefs.getAndMaybeResetDownloadsToday() - sequences = rn.Sequences(downloadsToday, self.prefs.stored_sequence_no) + if directory: + try: + path = gio.File(directory) + # first delete any files in the temp directory + # assume there are no directories in the temp directory + file_attributes = "standard::name,standard::type" + children = path.enumerate_children(file_attributes) + for child in children: + f = path.get_child(child.get_name()) + f.delete(cancellable=None) + path.delete(cancellable=None) + logger.debug("Deleted directory %s", directory) + except gio.Error, inst: + logger.error("Failure deleting temporary folder %s", directory) + logger.error(inst) + + + # # # + # Preferences + # # # - self.downloadStats = DownloadStats() - # set the number of seconds gap with which to measure download time remaing - self.downloadTimeGap = 3 - - #locks for threadsafe file downloading and stats gathering - self.fileRenameLock = Lock() - self.fileSequenceLock = Lock() - self.statsLock = Lock() - self.downloadedFilesLock = Lock() - - # log window, in dialog format - # used for displaying download information to the user - - log_dialog = LogDialog(self) - - - self.volumeMonitor = None - if self.usingVolumeMonitor(): - self.startVolumeMonitor() + def _init_prefs(self): + self.prefs = prefsrapid.RapidPreferences() + self.prefs.notify_add(self.on_preference_changed) # flag to indicate whether the user changed some preferences that # indicate the image and backup devices should be setup again - self.rerunSetupAvailableImageAndVideoMedia = False - self.rerunSetupAvailableBackupMedia = False - - # flag to indicate the user changes some preferences and the display - # of sample names and subfolders needs to be refreshed - self.refreshGeneratedSampleSubfolderAndName = False - - # counter to indicate how many threads need their sample names and subfolders regenerated because the user - # changes their prefs at the same time as devices were being scanned - self.noAfterScanRefreshGeneratedSampleSubfolderAndName = 0 - - # flag to indicate the user changes some preferences and the display - # of sample download folders needs to be refreshed - self.refreshSampleDownloadFolder = False - self.noAfterScanRefreshSampleDownloadFolders = 0 + self.rerun_setup_available_image_and_video_media = False + self.rerun_setup_available_backup_media = False # flag to indicate that the preferences dialog window is being # displayed to the user - self.preferencesDialogDisplayed = False - - # set up tree view display to display image devices and download status - media_collection_treeview = MediaTreeView(self) - - self.media_collection_vbox.pack_start(media_collection_treeview) - - #Selection display - self.selection_vbox = SelectionVBox(self) - self.selection_hbox.pack_start(self.selection_vbox, padding=12) - self.set_display_selection(self.prefs.display_selection) - self.set_display_preview_folders(self.prefs.display_preview_folders) - - self.backupVolumes = {} + self.preferences_dialog_displayed = False - #Help button and download buttons - self._setupDownloadbuttons() - - #status bar progress bar - self.download_progressbar = gtk.ProgressBar() - self.download_progressbar.set_size_request(150, -1) - self.download_progressbar.show() - self.download_progressbar_hbox.pack_start(self.download_progressbar, expand=False, - fill=0) + # flag to indicate that the user has modified the download today + # related values in the preferences dialog window + self.refresh_downloads_today = False - - # menus - - #preview panes - self.menu_display_selection.set_active(self.prefs.display_selection) - self.menu_preview_folders.set_active(self.prefs.display_preview_folders) + self.downloads_today_tracker = self.prefs.get_downloads_today_tracker() - #preview columns in pane - if not DOWNLOAD_VIDEO: - self.menu_type_column.set_active(False) - self.menu_type_column.set_sensitive(False) + downloads_today = self.downloads_today_tracker.get_and_maybe_reset_downloads_today() + if downloads_today > 0: + logger.info("Downloads that have occurred so far today: %s", downloads_today) else: - self.menu_type_column.set_active(self.prefs.display_type_column) - self.menu_size_column.set_active(self.prefs.display_size_column) - self.menu_filename_column.set_active(self.prefs.display_filename_column) - self.menu_device_column.set_active(self.prefs.display_device_column) - self.menu_path_column.set_active(self.prefs.display_path_column) - - self.menu_clear.set_sensitive(False) + logger.info("No downloads have occurred so far today") - need_job_code_for_renaming = self.needJobCodeForRenaming() - self.menu_select_all_without_job_code.set_sensitive(need_job_code_for_renaming) - self.menu_select_all_with_job_code.set_sensitive(need_job_code_for_renaming) - - #job code initialization - self.last_chosen_job_code = None - self.prompting_for_job_code = False - - #check to see if the download folder exists and is writable - displayPreferences_2 = not self.checkDownloadPathOnStartup() - displayPreferences = displayPreferences or displayPreferences_2 - - if self.prefs.device_autodetection == False: - displayPreferences_2 = not self.checkImageDevicePathOnStartup() - displayPreferences = displayPreferences or displayPreferences_2 + self.downloads_today_value = Value(c_int, + self.downloads_today_tracker.get_raw_downloads_today()) + self.downloads_today_date_value = Array(c_char, + self.downloads_today_tracker.get_raw_downloads_today_date()) + self.day_start_value = Array(c_char, + self.downloads_today_tracker.get_raw_day_start()) + self.refresh_downloads_today_value = Value(c_bool, False) + self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no) + self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no()) + self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no()) + self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value()) - #setup download and backup mediums, initiating scans - self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences) - - #adjust viewport size for displaying media - #this is important because the code in MediaTreeView.addCard() is inaccurate at program startup - - if media_collection_treeview.mapThreadToRow: - height = self.media_collection_viewport.size_request()[1] - self.media_collection_scrolledwindow.set_size_request(-1, height) - else: - # don't allow the media collection to be absolutely empty - self.media_collection_scrolledwindow.set_size_request(-1, 47) - - self.download_button.grab_default() - # for some reason, the grab focus command is not working... unsure why - self.download_button.grab_focus() + self.prefs.program_version = __version__ - if displayPreferences: - PreferencesDialog(self) - - - - @dbus.service.method (config.DBUS_NAME, - in_signature='', out_signature='b') - def is_running (self): - return self.running + def _check_for_sequence_value_use(self): + self.uses_stored_sequence_no_value.value = self.prefs.any_pref_uses_stored_sequence_no() + self.uses_session_sequece_no_value.value = self.prefs.any_pref_uses_session_sequece_no() + self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value() - @dbus.service.method (config.DBUS_NAME, - in_signature='', out_signature='') - def start (self): - if self.is_running(): - self.rapidapp.present() - else: - self.running = True -# if not using_gio: - self.main() -# else: -# mainloop = gobject.MainLoop() -# mainloop.run() - self.running = False - - def setTestingEnv(self): - #self.prefs.program_version = '0.0.8~b7' - p = ['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM', 'Text', '-', '', rn.SEQUENCES, rn.DOWNLOAD_SEQ_NUMBER, rn.SEQUENCE_NUMBER_3, 'Text', '-iso', '', 'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', '', 'Filename', 'Extension', 'lowercase'] - v = ['Date time', 'Video date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Video date', 'HHMM', 'Text', '-', '', 'Sequences', 'Downloads today', 'One digit', 'Text', '-', '', 'Metadata', 'Width', '', 'Text', 'x', '', 'Metadata', 'Height', '', 'Filename', 'Extension', 'lowercase'] - f = '/home/damon/store/rapid-dump' - self.prefs.image_rename = p - self.prefs.video_rename = v - self.prefs.download_folder = f - self.prefs.video_download_folder = f - - - def _setupIcons(self): - icons = ['rapid-photo-downloader-downloaded', - 'rapid-photo-downloader-downloaded-with-error', - 'rapid-photo-downloader-downloaded-with-warning', - 'rapid-photo-downloader-download-pending', - 'rapid-photo-downloader-jobcode'] - - icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] - common.register_iconsets(icon_list) - - def displayFreeSpace(self): + def on_preference_changed(self, key, value): """ - Displays the amount of space free on the filesystem the files will be downloaded to. - Also displays backup volumes / path being used. + Called when user changes the program's preferences """ - msg = '' - if using_gio and os.path.isdir(self.prefs.download_folder): - folder = gio.File(self.prefs.download_folder) - fileInfo = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) - free = common.formatSizeForUser(fileInfo.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)) - msg = " " + _("%(free)s available") % {'free': free} + logger.debug("Preference change detected: %s", key) - - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} - else: - msg2 = self.displayBackupVolumes() + + if key == 'show_log_dialog': + self.menu_log_window.set_active(value) + elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: + self.rerun_setup_available_image_and_video_media = True + if not self.preferences_dialog_displayed: + self.post_preference_change() - if msg: - msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} - else: - msg = msg2 - - self.rapid_statusbar.push(self.statusbar_context_id, msg) - - def checkImageDevicePathOnStartup(self): - msg = None - if not os.path.isdir(self.prefs.device_location): - msg = _("Sorry, this device location does not exist:\n%(path)s\n\nPlease resolve the problem, or modify your preferences." % {"path": self.prefs.device_location}) - - if msg: - sys.stderr.write(msg +'\n') - misc.run_dialog(_("Problem with Device Location Folder"), msg, - self, - gtk.MESSAGE_ERROR) - return False - else: - return True + elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: + self.rerun_setup_available_backup_media = True + if not self.preferences_dialog_displayed: + self.post_preference_change() + + # Downloads today and stored sequence numbers are kept in shared memory, + # so that the subfolderfile daemon process can access and modify them - def checkDownloadPathOnStartup(self): - if DOWNLOAD_VIDEO: - paths = ((self.prefs.download_folder, _('Photo')), (self.prefs.video_download_folder, _('Video'))) - else: - paths = ((self.prefs.download_folder, _('Photo')),) - msg = '' - noProblems = 0 - for path, file_type in paths: - if not os.path.isdir(path): - msg += _("The %(file_type)s Download Folder does not exist.\n") % {'file_type': file_type} - noProblems += 1 + # Note, totally ignore any changes in downloads today, as it + # is modified in a special manner via a tracking class + + elif key == 'stored_sequence_no': + if type(value) <> types.IntType: + logger.critical("Stored sequence number value is malformed") else: - #unfortunately 'os.access(self.prefs.download_folder, os.W_OK)' is not reliable - try: - tempWorkingDir = tempfile.mkdtemp(prefix='rapid-tmp-', - dir=path) - except: - noProblems += 1 - msg += _("The %(file_type)s Download Folder exists but cannot be written to.\n") % {'file_type': file_type} - else: - os.rmdir(tempWorkingDir) + self.stored_sequence_value.value = value + + elif key in ['image_rename', 'subfolder', 'video_rename', 'video_subfolder']: + # Check if stored sequence no is being used + self._check_for_sequence_value_use() - if msg: - msg = _("Sorry, problems were encountered with your download folders. Please fix the problems or modify the preferences.\n\n") + msg - sys.stderr.write(msg) - if noProblems == 1: - title = _("Problem with Download Folder") - else: - title = _("Problem with Download Folders") + #~ elif key == 'job_codes': + #~ # update job code list in left pane + #~ self.selection_vbox.update_job_code_combo() - misc.run_dialog(title, msg, - self, - gtk.MESSAGE_ERROR) - return False - else: - return True + elif key in ['download_folder', 'video_download_folder']: + self.display_free_space() - def checkPreferencesOnStartup(self): - prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder) - if not prefsOk: - msg = _("There is an error in the program preferences.") - msg += " " + _("Some preferences will be reset.") - # do not use cmd_line here, as this is a genuine error - sys.stderr.write(msg +'\n') - return prefsOk - - def needJobCodeForRenaming(self): - return rn.usesJobCode(self.prefs.image_rename) or rn.usesJobCode(self.prefs.subfolder) or rn.usesJobCode(self.prefs.video_rename) or rn.usesJobCode(self.prefs.video_subfolder) - - def assignJobCode(self, code): - """ assign job code (which may be empty) to global variable and update user preferences - - Update preferences only if code is not empty. Do not duplicate job code. - """ - global job_code - if code == None: - code = '' - job_code = code - - if job_code: - #add this value to job codes preferences - #delete any existing value which is the same - #(this way it comes to the front, which is where it should be) - #never modify self.prefs.job_codes in place! (or prefs become screwed up) + def post_preference_change(self): + if self.rerun_setup_available_image_and_video_media: + + logger.info("Download device settings preferences were changed.") - jcs = self.prefs.job_codes - while code in jcs: - jcs.remove(code) + self.thumbnails.clear_all() + self.setup_devices(on_startup = False, on_preference_change = True, block_auto_start = True) + self._set_device_collection_size() + + if self.main_notebook.get_current_page() == 1: # preview of file + self.main_notebook.set_current_page(0) - self.prefs.job_codes = [code] + jcs + self.rerun_setup_available_image_and_video_media = False - - def getShowWarningDownloadingFromCamera(self): - if self.prefs.show_warning_downloading_from_camera: - cmd_line(_("Displaying warning about downloading directly from camera")) - d = ShowWarningDialog(self.widget, self.gotShowWarningDownloadingFromCamera) + if self.rerun_setup_available_backup_media: + if self.using_volume_monitor(): + self.start_volume_monitor() + logger.info("Backup preferences were changed.") - def gotShowWarningDownloadingFromCamera(self, dialog, showWarningAgain): - dialog.destroy() - self.prefs.show_warning_downloading_from_camera = showWarningAgain - - def getUseDevice(self, path, volume, autostart): - """ Prompt user whether or not to download from this device """ - - cmd_line(_("Prompting whether to use %s" % volume.get_name(limit=0))) - d = UseDeviceDialog(self.widget, path, volume, autostart, self.gotUseDevice) - - def gotUseDevice(self, dialog, userSelected, permanent_choice, path, volume, autostart): - """ User has chosen whether or not to use a device to download from """ - dialog.destroy() - - if userSelected: - if permanent_choice and path not in self.prefs.device_whitelist: - # do not do a list append operation here without the assignment, or the preferences will not be updated! - if len(self.prefs.device_whitelist): - self.prefs.device_whitelist = self.prefs.device_whitelist + [path] - else: - self.prefs.device_whitelist = [path] - self.initiateScan(path, volume, autostart) + logger.info("self.refreshBackupMedia()") - elif permanent_choice and path not in self.prefs.device_blacklist: - # do not do a list append operation here without the assignment, or the preferences will not be updated! - if len(self.prefs.device_blacklist): - self.prefs.device_blacklist = self.prefs.device_blacklist + [path] - else: - self.prefs.device_blacklist = [path] - - def _getJobCode(self, postJobCodeEntryCB, autoStart, downloadSelected): - """ prompt for a job code """ - - if not self.prompting_for_job_code: - cmd_line(_("Prompting for Job Code")) - self.prompting_for_job_code = True - j = JobCodeDialog(self.widget, self.prefs.job_codes, self.last_chosen_job_code, postJobCodeEntryCB, autoStart, downloadSelected, False) - else: - cmd_line(_("Already prompting for Job Code, do not prompt again")) - - def getJobCode(self, autoStart=True, downloadSelected=False): - """ called from the copyphotos thread, or when the user clicks one of the two download buttons""" - - self._getJobCode(self.gotJobCode, autoStart, downloadSelected) - - def gotJobCode(self, dialog, userChoseCode, code, autoStart, downloadSelected): - dialog.destroy() - self.prompting_for_job_code = False - - if userChoseCode: - self.assignJobCode(code) - self.last_chosen_job_code = code - self.selection_vbox.selection_treeview.apply_job_code(code, overwrite=False, to_all_rows = not downloadSelected) - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = downloadSelected) - if downloadSelected or not autoStart: - cmd_line(_("Starting downloads")) - self.startDownload(threads) - else: - # autostart is true - cmd_line(_("Starting downloads that have been waiting for a Job Code")) - for w in workers.getWaitingForJobCodeWorkers(): - w.startStop() - - else: - # user cancelled - for w in workers.getWaitingForJobCodeWorkers(): - w.waitingForJobCode = False - - if autoStart: - for w in workers.getAutoStartWorkers(): - w.autoStart = False + self.rerun_setup_available_backup_media = False - def addFile(self, mediaFile): - self.selection_vbox.add_file(mediaFile) - - def update_status_post_download(self, treerowref): - self.selection_vbox.selection_treeview.update_status_post_download(treerowref) + if self.refresh_downloads_today: + self.downloads_today_value.value = self.downloads_today_tracker.get_raw_downloads_today() + self.downloads_today_date_value.value = self.downloads_today_tracker.get_raw_downloads_today_date() + self.day_start_value.value = self.downloads_today_tracker.get_raw_day_start() + self.refresh_downloads_today_value.value = True + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - def on_menu_size_column_toggled(self, widget): - self.prefs.display_size_column = widget.get_active() - self.selection_vbox.selection_treeview.display_size_column(self.prefs.display_size_column) - - def on_menu_type_column_toggled(self, widget): - self.prefs.display_type_column = widget.get_active() - self.selection_vbox.selection_treeview.display_type_column(self.prefs.display_type_column) - - def on_menu_filename_column_toggled(self, widget): - self.prefs.display_filename_column = widget.get_active() - self.selection_vbox.selection_treeview.display_filename_column(self.prefs.display_filename_column) - - def on_menu_path_column_toggled(self, widget): - self.prefs.display_path_column = widget.get_active() - self.selection_vbox.selection_treeview.display_path_column(self.prefs.display_path_column) - - def on_menu_device_column_toggled(self, widget): - self.prefs.display_device_column = widget.get_active() - self.selection_vbox.selection_treeview.display_device_column(self.prefs.display_device_column) - - def checkIfFirstTimeProgramEverRun(self): + + + # # # + # Main app window management and setup + # # # + + def _init_pynotify(self): """ - if this is the first time the program has been run, then - might need to create default directories + Initialize system notification messages """ - if len(self.prefs.program_version) == 0: - path = getDefaultPhotoLocation(ignore_missing_dir=True) - if not os.path.isdir(path): - cmd_line(_("Creating photo download folder %(folder)s") % {'folder':path}) - try: - os.makedirs(path) - self.prefs.download_folder = path - except: - cmd_line(_("Failed to create default photo download folder %(folder)s") % {'folder':path}) - if DOWNLOAD_VIDEO: - path = getDefaultVideoLocation(ignore_missing_dir=True) - if not os.path.isdir(path): - cmd_line(_("Creating video download folder %(folder)s") % {'folder':path}) - try: - os.makedirs(path) - self.prefs.video_download_folder = path - except: - cmd_line(_("Failed to create default video download folder %(folder)s") % {'folder':path}) - - def checkForUpgrade(self, runningVersion): - """ Checks if the running version of the program is different from the version recorded in the preferences. - - If the version is different, then the preferences are checked to see whether they should be upgraded or not. - - returns True if program preferences window should be opened """ - - displayPrefs = upgraded = False - previousVersion = self.prefs.program_version - if len(previousVersion) > 0: - # the program has been run previously for this user - - pv = common.pythonifyVersion(previousVersion) - rv = common.pythonifyVersion(runningVersion) - - title = PROGRAM_NAME - imageRename = subfolder = None - - if pv != rv: - if pv > rv: - prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder) - - msg = _("A newer version of this program was previously run on this computer.\n\n") - if prefsOk: - msg += _("Program preferences appear to be valid, but please check them to ensure correct operation.") - else: - msg += _("Sorry, some preferences are invalid and will be reset.") - sys.stderr.write(_("Warning:") + " %s\n" % msg) - misc.run_dialog(title, msg) - displayPrefs = True - - else: - cmd_line(_("This version of the program is newer than the previously run version. Checking preferences.")) - - if rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder, previousVersion): - upgraded, imageRename, subfolder = rn.upgradePreferencesToCurrent(self.prefs.image_rename, self.prefs.subfolder, previousVersion) - if upgraded: - self.prefs.image_rename = imageRename - self.prefs.subfolder = subfolder - cmd_line(_("Preferences were modified.")) - msg = _('This version of the program uses different preferences than the old version. Your preferences have been updated.\n\nPlease check them to ensure correct operation.') - misc.run_dialog(title, msg) - displayPrefs = True - else: - cmd_line(_("No preferences needed to be changed.")) - else: - msg = _('This version of the program uses different preferences than the old version. Some of your previous preferences were invalid, and could not be updated. They will be reset.') - sys.stderr.write(msg + "\n") - misc.run_dialog(title, msg) - displayPrefs = True - - - return displayPrefs - - def initPyNotify(self): if not pynotify.init("TestCaps"): - sys.stderr.write(_("Problem using pynotify.") + "\n") - sys.exit(1) - - capabilities = {'actions': False, - 'body': False, - 'body-hyperlinks': False, - 'body-images': False, - 'body-markup': False, - 'icon-multi': False, - 'icon-static': False, - 'sound': False, - 'image/svg+xml': False, - 'append': False} - - caps = pynotify.get_server_caps () - if caps is None: - sys.stderr.write(_("Failed to receive pynotify server capabilities.") + "\n") - sys.exit (1) - - for cap in caps: - capabilities[cap] = True + logger.critical("Problem using pynotify.") + gtk.main_quit() do_not_size_icon = False - self.notification_icon_size = 48 + self.notification_icon_size = 48 try: info = pynotify.get_server_info() except: - cmd_line(_("Warning: desktop environment notification server is incorrectly configured.")) + logger.warning("Desktop environment notification server is incorrectly configured.") else: try: if info["name"] == 'notify-osd': @@ -5390,1074 +2494,511 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): else: self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size( paths.share_dir('glade3/rapid-photo-downloader.svg'), - self.notification_icon_size, self.notification_icon_size) + self.notification_icon_size, self.notification_icon_size) - - - def usingVolumeMonitor(self): + def _init_widgets(self): """ - Returns True if programs needs to use gio or gnomevfs volume monitor + Initialize widgets in the main window, and variables that point to them """ + builder = gtk.Builder() + self.builder = builder + builder.add_from_file(paths.share_dir("glade3/rapid.ui")) + self.rapidapp = builder.get_object("rapidapp") + self.main_vpaned = builder.get_object("main_vpaned") + self.main_notebook = builder.get_object("main_notebook") + self.download_action = builder.get_object("download_action") - return (self.prefs.device_autodetection or - (self.prefs.backup_images and - self.prefs.backup_device_autodetection - )) + self.download_progressbar = builder.get_object("download_progressbar") + self.rapid_statusbar = builder.get_object("rapid_statusbar") + self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") + self.device_collection_scrolledwindow = builder.get_object("device_collection_scrolledwindow") + self.next_image_action = builder.get_object("next_image_action") + self.prev_image_action = builder.get_object("prev_image_action") + self.menu_log_window = builder.get_object("menu_log_window") + self.speed_label = builder.get_object("speed_label") - - def startVolumeMonitor(self): - if not self.volumeMonitor: - self.volumeMonitor = VMonitor(self) - - def displayBackupVolumes(self): - """ - Create a message to be displayed to the user showing which backup volumes will be used - """ - message = '' + # Only enable this action when actually displaying a preview + self.next_image_action.set_sensitive(False) + self.prev_image_action.set_sensitive(False) - paths = self.backupVolumes.keys() - i = 0 - v = len(paths) - prefix = '' - for b in paths: - if v > 1: - if i < (v -1) and i > 0: - prefix = ', ' - elif i == (v - 1) : - prefix = " " + _("and") + " " - i += 1 - message = "%s%s'%s'" % (message, prefix, self.backupVolumes[b].get_name()) - - if v > 1: - message = _("Using backup devices") + " %s" % message - elif v == 1: - message = _("Using backup device") + " %s" % message - else: - message = _("No backup devices detected") - - return message + # About dialog + builder.add_from_file(paths.share_dir("glade3/about.ui")) + self.about = builder.get_object("about") - def searchForPsd(self): - """ - Check to see if user preferences are to automatically search for Portable Storage Devices or not - """ - return self.prefs.device_autodetection_psd and self.prefs.device_autodetection + builder.connect_signals(self) + self.preview_image = PreviewImage(self, builder) - def isGProxyShadowMount(self, gMount): - - """ gvfs GProxyShadowMount is used for the camera itself, not the data in the memory card """ - if using_gio: - if hasattr(gMount, 'is_shadowed'): - return gMount.is_shadowed() - else: - return str(type(gMount)).find('GProxyShadowMount') >= 0 - else: - return False - - def isCamera(self, volume): - if using_gio: - try: - return volume.get_root().query_filesystem_info(gio.FILE_ATTRIBUTE_GVFS_BACKEND).get_attribute_as_string(gio.FILE_ATTRIBUTE_GVFS_BACKEND) == 'gphoto2' - except: - return False - else: - return False - - def workerHasThisPath(self, path): - havePath= False - for w in workers.getNonFinishedWorkers(): - if w.cardMedia.path == path: - havePath = True - break - return havePath + thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow') + self.thumbnails = ThumbnailDisplay(self) + thumbnails_scrolledwindow.add(self.thumbnails) - def on_volume_mounted(self, monitor, mount): - """ - callback run when gnomevfs indicates a new volume - has been mounted - """ + #collection of devices from which to download + self.device_collection_viewport = builder.get_object("device_collection_viewport") + self.device_collection = DeviceCollection(self) + self.device_collection_viewport.add(self.device_collection) - if self.usingVolumeMonitor(): - volume = Volume(mount) - path = volume.get_path() - - if path in self.prefs.device_blacklist and self.searchForPsd(): - cmd_line(_("Device %(device)s (%(path)s) ignored") % { - 'device': volume.get_name(limit=0), 'path': path}) - else: - if not self.isGProxyShadowMount(mount): - self._printDetectedDevice(volume.get_name(limit=0), path) - - isBackupVolume = self.checkIfBackupVolume(path) - - if isBackupVolume: - if path not in self.backupVolumes: - self.backupVolumes[path] = volume - self.displayFreeSpace() - - elif self.prefs.device_autodetection and (media.is_DCIM_Media(path) or self.searchForPsd()): - if self.isCamera(volume.volume): - self.getShowWarningDownloadingFromCamera() - if self.searchForPsd() and path not in self.prefs.device_whitelist: - # prompt user if device should be used or not - self.getUseDevice(path, volume, self.prefs.auto_download_upon_device_insertion) - else: - self._printAutoStart(self.prefs.auto_download_upon_device_insertion) - self.initiateScan(path, volume, self.prefs.auto_download_upon_device_insertion) - - def initiateScan(self, path, volume, autostart): - """ initiates scan of image device""" - cardMedia = CardMedia(path, volume) - i = workers.getNextThread_id() - - workers.append(CopyPhotos(i, self, self.fileRenameLock, - self.fileSequenceLock, self.statsLock, - self.downloadedFilesLock, self.downloadStats, - autostart, cardMedia)) - - - self.setDownloadButtonSensitivity() - self.startScan() - - - def on_volume_unmounted(self, monitor, volume): - """ - callback run when gnomevfs indicates a volume - has been unmounted - """ + #error log window + self.error_log = errorlog.ErrorLog(self) - v = Volume(volume) - path = v.get_path() - - # four scenarios - - # volume is waiting to be scanned - # the volume has been scanned but downloading has not yet started - # images are being downloaded from volume (it must be a messy unmount) - # images finished downloading from volume - - if path: - # first scenario - - for w in workers.getReadyToStartWorkers(): - if w.cardMedia.volume: - if w.cardMedia.volume.volume == volume: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - # second scenario - for w in workers.getReadyToDownloadWorkers(): - if w.cardMedia.volume: - if w.cardMedia.volume.volume == volume: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - - # fourth scenario - nothing to do - - # remove backup volumes - if path in self.backupVolumes: - del self.backupVolumes[path] - self.displayFreeSpace() - - # may need to disable download button - self.setDownloadButtonSensitivity() + # monitor to handle mounts and dismounts + self.vmonitor = None + # track scan ids for mount paths - very useful when a device is unmounted + self.mounts_by_path = {} - - def clearCompletedDownloads(self): - """ - clears the display of completed downloads - """ - - for w in workers.getFinishedWorkers(): - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - - - + # Download action state + self.download_action_is_download = True - def clearNotStartedDownloads(self): - """ - Clears the display of the download and instructs the thread not to run - """ + # Track the time a download commences + self.download_start_time = None - for w in workers.getNotDownloadingWorkers(): - media_collection_treeview.removeCard(w.thread_id) - workers.disableWorker(w.thread_id) - - def checkIfBackupVolume(self, path): - """ - Checks to see if backups are enabled and path represents a valid backup location + # Whether a system wide notifcation message should be shown + # after a download has occurred in parallel + self.display_summary_notification = False - Checks against user preferences. - """ - identifiers = [self.prefs.backup_identifier] - if DOWNLOAD_VIDEO: - identifiers.append(self.prefs.video_backup_identifier) - if self.prefs.backup_images: - if self.prefs.backup_device_autodetection: - if media.isBackupMedia(path, identifiers): - return True - elif path == self.prefs.backup_location: - # user manually specified the path - return True - return False + # Values used to display how much longer a download will take + self.time_remaining = downloadtracker.TimeRemaining() + self.time_check = downloadtracker.TimeCheck() - def _printDetectedDevice(self, volume_name, path): - cmd_line (_("Detected %(device)s with path %(path)s") % {'device': volume_name, 'path': path}) + + def _set_window_size(self): + """ + Remember the window size from the last time the program was run, or + set a default size + """ - def _printAutoStart(self, autoStart): - if autoStart: - cmd_line(_("Automatically start download is true") ) + if self.prefs.main_window_maximized: + self.rapidapp.maximize() + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + config.DEFAULT_WINDOW_HEIGHT) + elif self.prefs.main_window_size_x > 0: + self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y) else: - cmd_line(_("Automatically start download is false") ) + # set a default size + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + config.DEFAULT_WINDOW_HEIGHT) - def setupAvailableImageAndBackupMedia(self, onStartup, onPreferenceChange, doNotAllowAutoStart): + + def _set_device_collection_size(self): """ - Sets up volumes for downloading from and backing up to - - onStartup should be True if the program is still starting, i.e. this is being called from the - program's initialization. - - onPreferenceChange should be True if this is being called as the result of a preference - being changed - - Removes any image media that are currently not downloaded, - or finished downloading + Set the size of the device collection scrolled window widget """ - self.clearNotStartedDownloads() - volumeList = [] - self.backupVolumes = {} - - if not workers.noDownloadingWorkers(): - self.downloadStats.clear() - self._resetDownloadInfo() + if self.device_collection.map_process_to_row: + height = self.device_collection_viewport.size_request()[1] + self.device_collection_scrolledwindow.set_size_request(-1, height) + else: + # don't allow the media collection to be absolutely empty + self.device_collection_scrolledwindow.set_size_request(-1, 47) - if self.usingVolumeMonitor(): - # either using automatically detected backup devices - # or download devices - for v in self.volumeMonitor.get_mounts(): - volume = Volume(v) #'volumes' are actually mounts (legacy variable name at work here) - path = volume.get_path(avoid_gnomeVFS_bug = True) - - if path: - if path in self.prefs.device_blacklist and self.searchForPsd(): - cmd_line(_("Device %(device)s (%(path)s) ignored") % { - 'device': volume.get_name(limit=0), - 'path': path}) - else: - if not self.isGProxyShadowMount(v): - self._printDetectedDevice(volume.get_name(limit=0), path) - isBackupVolume = self.checkIfBackupVolume(path) - if isBackupVolume: - #backupPath = os.path.join(path, self.prefs.backup_identifier) - self.backupVolumes[path] = volume - elif self.prefs.device_autodetection and (media.is_DCIM_Media(path) or self.searchForPsd()): - volumeList.append((path, volume)) - + def on_rapidapp_window_state_event(self, widget, event): + """ Records the window maximization state in the preferences.""" - if not self.prefs.device_autodetection: - # user manually specified the path from which to download - path = self.prefs.device_location - if path: - cmd_line(_("Using manually specified path") + " %s" % path) - volumeList.append((path, None)) - - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info associated with it - self.backupVolumes[self.prefs.backup_location] = None + if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: + self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - self.displayFreeSpace() - # add each memory card / other device to the list of threads + def _setup_buttons(self): + thumbnails_button = self.builder.get_object("thumbnails_button") + image = gtk.image_new_from_file(paths.share_dir('glade3/thumbnails_icon.png')) + thumbnails_button.set_image(image) - if doNotAllowAutoStart: - autoStart = False - else: - autoStart = (not onPreferenceChange) and ((self.prefs.auto_download_at_startup and onStartup) or (self.prefs.auto_download_upon_device_insertion and not onStartup)) + preview_button = self.builder.get_object("preview_button") + image = gtk.image_new_from_file(paths.share_dir('glade3/photo_icon.png')) + preview_button.set_image(image) - self._printAutoStart(autoStart) + next_image_button = self.builder.get_object("next_image_button") + image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON) + next_image_button.set_image(image) - shownWarning = False - - for i in range(len(volumeList)): - path, volume = volumeList[i] - if volume: - if self.isCamera(volume.volume) and not shownWarning: - self.getShowWarningDownloadingFromCamera() - shownWarning = True - if self.searchForPsd() and path not in self.prefs.device_whitelist: - # prompt user to see if device should be used or not - self.getUseDevice(path, volume, autoStart) - else: - self.initiateScan(path, volume, autoStart) - - def refreshBackupMedia(self): - """ - Setup the backup media - - Assumptions: this is being called after the user has changed their preferences AND download media has already been setup - """ - self.backupVolumes = {} - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info associated with it - self.backupVolumes[self.prefs.backup_location] = None - else: - for v in self.volumeMonitor.get_mounts(): - volume = Volume(v) - path = volume.get_path(avoid_gnomeVFS_bug = True) - if path: - if self.checkIfBackupVolume(path): - # is a backup volume - if path not in self.backupVolumes: - # ensure it is not in a list of workers which have not started downloading - # if it is, remove it - for w in workers.getNotDownloadingAndNotFinishedWorkers(): - if w.cardMedia.path == path: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - - downloading_workers = [] - for w in workers.getDownloadingWorkers(): - downloading_workers.append(w) - - for w in downloading_workers: - if w.cardMedia.path == path: - # the user is trying to backup to a device that is currently being downloaded from..... we don't normally allow that, but what to do? - cmd_line(_("Warning: backup device %(device)s is currently being downloaded from") % {'device': volume.get_name(limit=0)}) - - self.backupVolumes[path] = volume - - self.displayFreeSpace() - - def _setupDownloadbuttons(self): - self.download_hbutton_box = gtk.HButtonBox() - self.download_hbutton_box.set_spacing(12) - self.download_hbutton_box.set_homogeneous(False) - - help_button = gtk.Button(stock=gtk.STOCK_HELP) - help_button.connect("clicked", self.on_help_button_clicked) - self.download_hbutton_box.pack_start(help_button) - self.download_hbutton_box.set_child_secondary(help_button, True) + prev_image_button = self.builder.get_object("prev_image_button") + image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON) + prev_image_button.set_image(image) - self.DOWNLOAD_SELECTED_LABEL = _("D_ownload Selected") - self.download_button_is_download = True - self.download_button = gtk.Button() - self.download_button.set_use_underline(True) - self.download_button.set_flags(gtk.CAN_DEFAULT) - self.download_selected_button = gtk.Button() - self.download_selected_button.set_use_underline(True) - self._set_download_button() - self.download_button.connect('clicked', self.on_download_button_clicked) - self.download_selected_button.connect('clicked', self.on_download_selected_button_clicked) - self.download_hbutton_box.set_layout(gtk.BUTTONBOX_END) - self.download_hbutton_box.pack_start(self.download_selected_button) - self.download_hbutton_box.pack_start(self.download_button) - self.download_hbutton_box.show_all() - self.buttons_hbox.pack_start(self.download_hbutton_box, - padding=hd.WINDOW_BORDER_SPACE) - - self.setDownloadButtonSensitivity() - - def set_display_selection(self, value): - if value: - self.selection_vbox.preview_table.show_all() - else: - self.selection_vbox.preview_table.hide() - self.selection_vbox.set_display_preview_folders(self.prefs.display_preview_folders) - - def set_display_preview_folders(self, value): - self.selection_vbox.set_display_preview_folders(value) - - def _resetDownloadInfo(self): - self.markSet = False - self.startTime = None - self.totalDownloadSize = self.totalDownloadedSoFar = 0 - self.totalDownloadSizeThisRun = self.totalDownloadedSoFarThisRun = 0 - # there is no need to clear self.timeRemaining, as when each thread is completed, it removes itself - - # this next value is used by the date time option "Download Time" - self.download_start_time = None - - global job_code - job_code = None + def _setup_icons(self): + icons = ['rapid-photo-downloader-jobcode',] + icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] + register_iconsets(icon_list) - def addToTotalDownloadSize(self, size): - self.totalDownloadSize += size - - def setOverallDownloadMark(self): - if not self.markSet: - self.markSet = True - self.totalDownloadSizeThisRun = self.totalDownloadSize - self.totalDownloadedSoFar - self.totalDownloadedSoFarThisRun = 0 - - self.startTime = time.time() - self.timeStatusBarUpdated = self.startTime - - self.timeMark = self.startTime - self.sizeMark = 0 - - def startOrResumeWorkers(self, threads): - - # resume any paused workers - for w in workers.getPausedDownloadingWorkers(): - w.startStop() - self.timeRemaining.setTimeMark(w) + def _setup_error_icons(self): + """ + hide display of warning and error symbols in the taskbar until they + are needed + """ + self.error_image = self.builder.get_object("error_image") + self.warning_image = self.builder.get_object("warning_image") + self.warning_vseparator = self.builder.get_object("warning_vseparator") + self.error_image.hide() + self.warning_image.hide() + self.warning_vseparator.hide() - # set the time that the download started - this is used - # in the "Download Time" date time renaming option. - self.setDownloadStartTime() - - - #start any new workers that have downloads pending - for i in threads: - workers[i].startStop() + def statusbar_message(self, msg): + self.rapid_statusbar.push(self.statusbar_context_id, msg) - if is_beta and verbose and False: - workers.printWorkerStatus() - - def setDownloadStartTime(self): - if not self.download_start_time: - self.download_start_time = datetime.datetime.now() + def statusbar_message_remove(self): + self.rapid_statusbar.pop(self.statusbar_context_id) - def updateOverallProgress(self, thread_id, bytesDownloaded, percentComplete): + def display_free_space(self): """ - Updates progress bar and status bar text with time remaining - to download images + Displays the amount of space free on the filesystem the files will be + downloaded to. + + Also displays backup volumes / path being used. (NOT IMPLEMENTED YET) """ - - self.totalDownloadedSoFar += bytesDownloaded - self.totalDownloadedSoFarThisRun += bytesDownloaded - - fraction = self.totalDownloadedSoFar / float(self.totalDownloadSize) - - self.download_progressbar.set_fraction(fraction) - - if percentComplete == 100.0: - self.menu_clear.set_sensitive(True) - self.timeRemaining.remove(thread_id) - - if self.downloadComplete(): - # finished all downloads - self.rapid_statusbar.push(self.statusbar_context_id, "") - self.download_button_is_download = True - self._set_download_button() - self.setDownloadButtonSensitivity() - cmd_line(_("All downloads complete")) - job_code = None - if is_beta and verbose and False: - workers.printWorkerStatus() - + photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True) + video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True) + if photo_dir and video_dir: + same_file_system = self.same_file_system(self.prefs.download_folder, + self.prefs.video_download_folder) else: - now = time.time() - self.timeRemaining.update(thread_id, bytesDownloaded) - - if now > (self.downloadTimeGap + self.timeMark): - amtTime = now - self.timeMark - self.timeMark = now - amtDownloaded = self.totalDownloadedSoFarThisRun - self.sizeMark - self.sizeMark = self.totalDownloadedSoFarThisRun - amtToDownload = float(self.totalDownloadSizeThisRun) - self.totalDownloadedSoFarThisRun - downloadSpeed = "%1.1f" % (amtDownloaded / 1048576 / amtTime) +_("MB/s") - self.speed_label.set_text(downloadSpeed) + same_file_system = False - timeRemaining = self.timeRemaining.timeRemaining() - if timeRemaining: - secs = int(timeRemaining) + dirs = [] + if photo_dir: + dirs.append((self.prefs.download_folder, _("photos"))) + if video_dir and not same_file_system: + dirs.append((self.prefs.video_download_folder, _("videos"))) + + msg = '' + if len(dirs) > 1: + msg = ' ' + _('Free space:') + ' ' + + for i in range(len(dirs)): + dir_info = dirs[i] + folder = gio.File(dir_info[0]) + file_info = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) + size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) + free = format_size_for_user(bytes=size) + if len(dirs) > 1: + #(videos) or (photos) will be appended to the free space message displayed to the + #user in the status bar. + #you should only translate this if your language does not use parantheses + file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]} + + #Freespace available on the filesystem for downloading to + #Displayed in status bar message on main window + msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type} + if i == 0: + #Inserted in the middle of the statusbar message concerning the amount of freespace + #Used to differentiate between two different file systems + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + msg += _("; ") + else: + #Inserted at the end of the statusbar message concerning the amount of freespace + #Used to differentiate between two different file systems + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + msg += _(".") - if secs == 0: - message = "" - elif secs == 1: - message = _("About 1 second remaining") - elif secs < 60: - message = _("About %i seconds remaining") % secs - elif secs == 60: - message = _("About 1 minute remaining") - else: - # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. - # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount - # of time the download has remainging, e.g. 'About 5:36 minutes remaining' - message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} - - self.rapid_statusbar.pop(self.statusbar_context_id) - self.rapid_statusbar.push(self.statusbar_context_id, message) - - - def resetSequences(self): - if self.downloadComplete(): - sequences.reset(self.prefs.getDownloadsToday(), self.prefs.stored_sequence_no) - - def notifyUserAllDownloadsComplete(self): - """ If all downloads are complete, if needed notify the user using libnotify - - Reset progress bar info""" - - if self.downloadComplete(): - if self.displayDownloadSummaryNotification: - message = _("All downloads complete") - if self.downloadStats.noImagesDownloaded: - filetype = file_types_by_number(self.downloadStats.noImagesDownloaded, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noImagesDownloaded, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - if self.downloadStats.noImagesSkipped: - filetype = file_types_by_number(self.downloadStats.noImagesSkipped, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noImagesSkipped, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - if self.downloadStats.noVideosDownloaded: - filetype = file_types_by_number(0, self.downloadStats.noVideosDownloaded) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noVideosDownloaded, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - if self.downloadStats.noVideosSkipped: - filetype = file_types_by_number(0, self.downloadStats.noVideosSkipped) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noVideosSkipped, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - if self.downloadStats.noWarnings: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noWarnings, - 'numberdownloaded': _("warnings")} - if self.downloadStats.noErrors: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noErrors, - 'numberdownloaded': _("errors")} - - n = pynotify.Notification(PROGRAM_NAME, message) - n.set_icon_from_pixbuf(self.application_icon) - n.show() - self.displayDownloadSummaryNotification = False # don't show it again unless needed - # download statistics are cleared in exitOnDownloadComplete() - self._resetDownloadInfo() - self.speed_label.set_text(' ') - self.displayFreeSpace() + else: + #Freespace available on the filesystem for downloading to + #Displayed in status bar message on main window + #e.g. 14.7GB available + msg = " " + _("%(free)s free") % {'free': free} + + if self.prefs.backup_images and False: #FIXME: skip this for now! + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} + else: + msg2 = self.displayBackupVolumes() #FIXME - def exitOnDownloadComplete(self): - if self.downloadComplete(): - if self.prefs.auto_exit: - if not (self.downloadStats.noErrors or self.downloadStats.noWarnings): - self.quit() - # since for whatever reason am not exiting, clear the download statistics - self.downloadStats.clear() - - - def downloadFailed(self, thread_id): - if workers.noDownloadingWorkers() == 0: - self.download_button_is_download = True - self._set_download_button() - self.setDownloadButtonSensitivity() - - def downloadComplete(self): - return self.totalDownloadedSoFar == self.totalDownloadSize - - def setDownloadButtonSensitivity(self): - - isSensitive = (workers.noReadyToDownloadWorkers() > 0 and - workers.noScanningWorkers() == 0 and - self.selection_vbox.selection_treeview.rows_available_for_download()) or \ - workers.noDownloadingWorkers() > 0 + if msg: + msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} + else: + msg = msg2 - if isSensitive: - self.download_button.props.sensitive = True - # download selected button sensitity is enabled only when the user selects something - self.selection_vbox.selection_treeview.update_download_selected_button() - self.menu_download_pause.props.sensitive = True - else: - self.download_button.props.sensitive = False - self.download_selected_button.props.sensitive = False - self.menu_download_pause.props.sensitive = False + msg = msg.rstrip() - return isSensitive - - - def on_rapidapp_destroy(self, widget): - """Called when the application is going to quit""" - - # save window and component sizes - self.prefs.hpaned_pos = self.selection_vbox.file_hpaned.get_position() - self.prefs.vpaned_pos = self.main_vpaned.get_position() - - x, y = self.rapidapp.get_size() - self.prefs.main_window_size_x = x - self.prefs.main_window_size_y = y - - workers.quitAllWorkers() - - self.flushevents() + self.statusbar_message(msg) - display_queue.close("w") - - - def on_rapidapp_window_state_event(self, widget, event): - """ Checkto see if the user maximized the main application window or not. """ - if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: - self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - - - def on_menu_clear_activate(self, widget): - self.clearCompletedDownloads() - widget.set_sensitive(False) - - def on_menu_refresh_activate(self, widget): - self.selection_vbox.selection_treeview.clear_all() - self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True) + def log_error(self, severity, problem, details, extra_detail=None): + """ + Display error and warning messages to user in log window + """ + self.error_log.add_message(severity, problem, details, extra_detail) - def on_menu_report_problem_activate(self, widget): - webbrowser.open("https://bugs.launchpad.net/rapid") + + def on_error_eventbox_button_press_event(self, widget, event): + self.prefs.show_log_dialog = True + self.error_log.widget.show() - def on_menu_get_help_online_activate(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/help.html") - - def on_menu_donate_activate(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/donate.html") - - def on_menu_translate_activate(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/translate.html") - - def on_menu_preferences_activate(self, widget): - """ Sets preferences for the application using dialog window """ - - PreferencesDialog(self) def on_menu_log_window_toggled(self, widget): active = widget.get_active() self.prefs.show_log_dialog = active if active: - log_dialog.widget.show() + self.error_log.widget.show() else: - log_dialog.widget.hide() - - def on_menu_display_selection_toggled(self, check_button): - self.prefs.display_selection = check_button.get_active() - - def on_menu_preview_folders_toggled(self, check_button): - self.prefs.display_preview_folders = check_button.get_active() - - def on_menu_zoom_out_activate(self, widget): - self.selection_vbox.zoom_out() - - def on_menu_zoom_in_activate(self, widget): - self.selection_vbox.zoom_in() - - def on_menu_select_all_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('all') - - def on_menu_select_all_photos_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('photos') + self.error_log.widget.hide() + + def notify_prefs_are_invalid(self, details): + title = _("Program preferences are invalid") + logger.critical(title) + self.log_error(severity=config.CRITICAL_ERROR, problem=title, + details=details) - def on_menu_select_all_videos_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('videos') - - def on_menu_select_none_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('none') - - def on_menu_select_all_with_job_code_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('withjobcode') - - def on_menu_select_all_without_job_code_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('withoutjobcode') - - - def on_menu_about_activate(self, widget): - """ Display about dialog box """ - - about = gtk.glade.XML(paths.share_dir(config.GLADE_FILE), "about").get_widget("about") - about.set_property("name", PROGRAM_NAME) - about.set_property("version", __version__) - about.run() - about.destroy() + + # # # + # Utility functions + # # # - def _set_download_button(self): + def files_of_type_present(self, files, file_type): """ - Sets download button to appropriate state + Returns true if there is at least one instance of the file_type + in the list of files to be copied """ + for rpd_file in files: + if rpd_file.file_type == file_type: + return True + return False - if self.download_button_is_download: - # This text will be displayed to the user on the Download / Pause button. - self.download_selected_button.set_label(self.DOWNLOAD_SELECTED_LABEL) - self.download_selected_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CONVERT, - gtk.ICON_SIZE_BUTTON)) - self.selection_vbox.selection_treeview.update_download_selected_button() - - self.download_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CONVERT, - gtk.ICON_SIZE_BUTTON)) - - if workers.noPausedWorkers(): - self.download_button.set_label(_("_Resume")) - self.download_selected_button.hide() - else: - self.download_button.set_label(_("_Download All")) - self.download_selected_button.show_all() - - else: - # button should indicate paused state - self.download_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_MEDIA_PAUSE, - gtk.ICON_SIZE_BUTTON)) - # This text will be displayed to the user on the Download / Pause button. - self.download_button.set_label(_("_Pause")) - self.download_selected_button.set_sensitive(False) - self.download_selected_button.hide() - - def on_menu_download_pause_activate(self, widget): - self.on_download_button_clicked(widget) - - def startScan(self): - if workers.noReadyToStartWorkers() > 0: - workers.startWorkers() - - def postStartDownloadTasks(self): - if workers.noDownloadingWorkers() > 1: - self.displayDownloadSummaryNotification = True - - # set button to display Pause - self.download_button_is_download = False - self._set_download_button() - - def startDownload(self, threads): - self.startOrResumeWorkers(threads) - self.postStartDownloadTasks() - - def pauseDownload(self): - for w in workers.getDownloadingWorkers(): - w.startStop() - # set button to display Download - if not self.download_button_is_download: - self.download_button_is_download = True - self._set_download_button() - - def on_download_button_clicked(self, widget): + def size_files_to_be_downloaded(self, files): """ - Handle download button click. - - Button is in one of three states: download all, resume, or pause. - - If download, a click indicates to start or resume a download run. - If pause, a click indicates to pause all running downloads. + Returns the total size of the files to be downloaded in bytes """ - if self.download_button_is_download: - if need_job_code_for_renaming and self.selection_vbox.selection_treeview.job_code_missing(False) and not self.prompting_for_job_code: - self.getJobCode(autoStart=False, downloadSelected=False) - else: - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = False) - self.startDownload(threads) - self._set_download_button() - else: - self.pauseDownload() - - def on_download_selected_button_clicked(self, widget): - # set the status of the selected workers to be downloading pending - if need_job_code_for_renaming and self.selection_vbox.selection_treeview.job_code_missing(True) and not self.prompting_for_job_code: - self.getJobCode(autoStart=False, downloadSelected=True) - else: - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = True) - self.startDownload(threads) - + size = 0 + for i in range(len(files)): + size += files[i].size - - def on_help_button_clicked(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/help.html") - - def on_preference_changed(self, key, value): - """ - Called when user changes the program's preferences + return size + + def check_download_folder_validity(self, files_by_scan_pid): """ + Checks validity of download folders based on the file types the user + is attempting to download. - if key == 'display_selection': - self.set_display_selection(value) - elif key == 'display_preview_folders': - self.set_display_preview_folders(value) - elif key == 'show_log_dialog': - self.menu_log_window.set_active(value) - elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: - self.rerunSetupAvailableImageAndVideoMedia = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: - self.rerunSetupAvailableBackupMedia = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['subfolder', 'image_rename', 'video_subfolder', 'video_rename']: - global need_job_code_for_renaming - need_job_code_for_renaming = self.needJobCodeForRenaming() - self.selection_vbox.set_job_code_display() - self.menu_select_all_without_job_code.set_sensitive(need_job_code_for_renaming) - self.menu_select_all_with_job_code.set_sensitive(need_job_code_for_renaming) - self.refreshGeneratedSampleSubfolderAndName = True - - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['download_folder', 'video_download_folder']: - self.refreshSampleDownloadFolder = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key == 'job_codes': - # update job code list in left pane - self.selection_vbox.update_job_code_combo() - - - def postPreferenceChange(self): - """ - Handle changes in program preferences after the preferences dialog window has been closed + If valid, returns a tuple of True and an empty list. + If invalid, returns a tuple of False and a list of the invalid directores. """ - if self.rerunSetupAvailableImageAndVideoMedia: - if self.usingVolumeMonitor(): - self.startVolumeMonitor() - cmd_line("\n" + _("Download device settings preferences were changed.")) + valid = True + invalid_dirs = [] + # first, check what needs to be downloaded - photos and / or videos + need_photo_folder = False + need_video_folder = False + while not need_photo_folder and not need_video_folder: + for scan_pid in files_by_scan_pid: + files = files_by_scan_pid[scan_pid] + if not need_photo_folder: + if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + need_photo_folder = True + if not need_video_folder: + if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): + need_video_folder = True - self.selection_vbox.selection_treeview.clear_all() - self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True) - if is_beta and verbose and False: - workers.printWorkerStatus() + # second, check validity + if need_photo_folder: + if not self.is_valid_download_dir(self.prefs.download_folder, + is_photo_dir=True): + valid = False + invalid_dirs.append(self.prefs.download_folder) - self.rerunSetupAvailableImageAndVideoMedia = False - - if self.rerunSetupAvailableBackupMedia: - if self.usingVolumeMonitor(): - self.startVolumeMonitor() - cmd_line("\n" + _("Backup preferences were changed.")) - - self.refreshBackupMedia() - self.rerunSetupAvailableBackupMedia = False - - if self.refreshGeneratedSampleSubfolderAndName: - cmd_line("\n" + _("Subfolder and filename preferences were changed.")) - for w in workers.getScanningWorkers(): - if not w.scanResultsStale: - w.scanResultsStale = True - self.noAfterScanRefreshGeneratedSampleSubfolderAndName += 1 + if need_video_folder: + if not self.is_valid_download_dir(self.prefs.video_download_folder, + is_photo_dir=False): + valid = False + invalid_dirs.append(self.prefs.video_download_folder) - self.selection_vbox.selection_treeview.refreshGeneratedSampleSubfolderAndName() - self.refreshGeneratedSampleSubfolderAndName = False - self.setDownloadButtonSensitivity() - - if self.refreshSampleDownloadFolder: - cmd_line("\n" + _("Download folder preferences were changed.")) - for w in workers.getScanningWorkers(): - if not w.scanResultsStaleDownloadFolder: - w.scanResultsStaleDownloadFolder = True - self.noAfterScanRefreshSampleDownloadFolders += 1 - - self.selection_vbox.selection_treeview.refreshSampleDownloadFolders() - self.refreshSampleDownloadFolder = False + return (valid, invalid_dirs) - def regenerateScannedDevices(self, thread_id): + def same_file_system(self, file1, file2): + """Returns True if the files / diretories are on the same file system """ - Regenerate the filenames / subfolders / download folders for this thread + f1 = gio.File(file1) + f2 = gio.File(file2) + f1_info = f1.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f2_info = f2.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + return f1_id == f2_id - The user must have adjusted their preferences as the device was being scanned + + def same_file(self, file1, file2): + """Returns True if the files / directories are the same """ + f1 = gio.File(file1) + f2 = gio.File(file2) - if self.noAfterScanRefreshSampleDownloadFolders: - # no point updating it if we're going to update it in the - # refresh of sample names and subfolders anway! - if not self.noAfterScanRefreshGeneratedSampleSubfolderAndName: - self.selection_vbox.selection_treeview.refreshSampleDownloadFolders(thread_id) - self.noAfterScanRefreshSampleDownloadFolders -= 1 - - if self.noAfterScanRefreshGeneratedSampleSubfolderAndName: - self.selection_vbox.selection_treeview.refreshGeneratedSampleSubfolderAndName(thread_id) - self.noAfterScanRefreshGeneratedSampleSubfolderAndName -= 1 - - + file_attributes = "id::file" + f1_info = f1.query_filesystem_info(file_attributes) + f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) + f2_info = f2.query_filesystem_info(file_attributes) + f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) + return f1_id == f2_id - - def on_error_eventbox_button_press_event(self, widget, event): - self.prefs.show_log_dialog = True - log_dialog.widget.show() - -class VMonitor: - """ Transistion to gvfs from gnomevfs""" - def __init__(self, app): - self.app = app - if using_gio: - self.vmonitor = gio.volume_monitor_get() - self.vmonitor.connect("mount-added", self.app.on_volume_mounted) - self.vmonitor.connect("mount-removed", self.app.on_volume_unmounted) + def is_valid_download_dir(self, path, is_photo_dir, show_error_in_log=False): + """ + Checks the following conditions: + Does the directory exist? + Is it writable? + + if show_error_in_log is True, then display warning in log window, using + is_photo_dir, which if true means the download directory is for photos, + if false, for Videos + """ + valid = False + if is_photo_dir: + download_folder_type = _("Photo") else: - self.vmonitor = gnomevfs.VolumeMonitor() - self.vmonitor.connect("volume-mounted", self.app.on_volume_mounted) - self.vmonitor.connect("volume-unmounted", self.app.on_volume_unmounted) + download_folder_type = _("Video") + try: + d = gio.File(path) + if not d.query_exists(cancellable=None): + logger.error("%s download folder does not exist: %s", + download_folder_type, path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder does not exist") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) + else: + file_attributes = "standard::type,access::can-read,access::can-write" + file_info = d.query_filesystem_info(file_attributes) + file_type = file_info.get_file_type() + + if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN: + logger.error("%s download folder is invalid: %s", + download_folder_type, path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder is invalid") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) + else: + # is the directory writable? + try: + temp_dir = tempfile.mkdtemp(prefix="rpd-tmp", dir=path) + valid = True + except: + logger.error("%s is not writable", path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder is not writable") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) + else: + f = gio.File(temp_dir) + f.delete(cancellable=None) - def get_mounts(self): - if using_gio: - return self.vmonitor.get_mounts() - else: - return self.vmonitor.get_mounted_volumes() + except gio.Error, inst: + logger.error("Error checking download directory %s", path) + logger.error(inst) -class Volume: - """ Transistion to gvfs from gnomevfs""" - def __init__(self, volume): - self.volume = volume - - def get_name(self, limit=config.MAX_LENGTH_DEVICE_NAME): - if using_gio: - v = self.volume.get_name() - else: - v = self.volume.get_display_name() - - if limit: - if len(v) > limit: - v = v[:limit] + '...' - return v + return valid + + + + # # # + # Process results and management + # # # - def get_path(self, avoid_gnomeVFS_bug = False): - if using_gio: - path = self.volume.get_root().get_path() - else: - uri = self.volume.get_activation_uri() - path = None - if avoid_gnomeVFS_bug: - # ugly hack to work around bug where gnomevfs.get_local_path_from_uri(uri) causes a crash - mediaLocation = "file://" + config.MEDIA_LOCATION - if uri.find(mediaLocation) == 0: - path = gnomevfs.get_local_path_from_uri(uri) - else: - path = gnomevfs.get_local_path_from_uri(uri) - return path + def _start_process_managers(self): + """ + Set up process managers. + + A task such as scanning a device or copying files is handled in its + own process. + """ - def get_icon_pixbuf(self, size): - """ returns icon for the volume, or None if not available""" + self.batch_size = 10 + self.batch_size_MB = 2 - return common.get_icon_pixbuf(using_gio, self.volume.get_icon(), size) + sequence_values = (self.downloads_today_value, + self.downloads_today_date_value, + self.day_start_value, + self.refresh_downloads_today_value, + self.stored_sequence_value, + self.uses_stored_sequence_no_value, + self.uses_session_sequece_no_value, + self.uses_sequence_letter_value) + + self.subfolder_file_manager = SubfolderFileManager( + self.subfolder_file_results, + sequence_values) - def unmount(self, callback): - self.volume.unmount(callback) - -class DownloadStats: - def __init__(self): - self.clear() - def adjust(self, size, noImagesDownloaded, noVideosDownloaded, noImagesSkipped, noVideosSkipped, noWarnings, noErrors): - self.downloadSize += size - self.noImagesDownloaded += noImagesDownloaded - self.noVideosDownloaded += noVideosDownloaded - self.noImagesSkipped += noImagesSkipped - self.noVideosSkipped += noVideosSkipped - self.noWarnings += noWarnings - self.noErrors += noErrors - - def clear(self): - self.noImagesDownloaded = self.noVideosDownloaded = self.noImagesSkipped = self.noVideosSkipped = 0 - self.downloadSize = 0 - self.noWarnings = self.noErrors = 0 - -class DownloadedFiles: - def __init__(self): - self.images = {} - - def add_download(self, name, extension, date_time, sub_seconds, sequence_number_used): - if name not in self.images: - self.images[name] = ([extension], date_time, sub_seconds, sequence_number_used) - else: - if extension not in self.images[name][0]: - self.images[name][0].append(extension) - + self.generate_folder = False + self.scan_manager = ScanManager(self.scan_results, self.batch_size, + self.generate_folder, self.device_collection.add_device) + self.copy_files_manager = CopyFilesManager(self.copy_files_results, + self.batch_size_MB) - def matching_pair(self, name, extension, date_time, sub_seconds): - """Checks to see if the image matches an image that has already been downloaded. - Image name (minus extension), exif date time, and exif subseconds are checked. + def scan_results(self, source, condition): + """ + Receive results from scan processes + """ + connection = self.scan_manager.get_pipe(source) - Returns -1 and a sequence number if the name, extension, and exif values match (i.e. it has already been downloaded) - Returns 0 and a sequence number if name and exif values match, but the extension is different (i.e. a matching RAW + JPG image) - Returns -99 and a sequence number of None if images detected with the same filenames, but taken at different times - Returns 1 and a sequence number of None if no match""" + conn_type, data = connection.recv() - if name in self.images: - if self.images[name][1] == date_time and self.images[name][2] == sub_seconds: - if extension in self.images[name][0]: - return (-1, self.images[name][3]) - else: - return (0, self.images[name][3]) + if conn_type == rpdmp.CONN_COMPLETE: + connection.close() + self.scan_manager.no_tasks -= 1 + size, file_type_counter, scan_pid = data + size = format_size_for_user(bytes=size) + results_summary, file_types_present = file_type_counter.summarize_file_count() + self.download_tracker.set_file_types_present(scan_pid, file_types_present) + logger.info('Found %s' % results_summary) + logger.info('Files total %s' % size) + self.device_collection.update_device(scan_pid, size) + self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0) + self.testing_auto_exit_trip_counter += 1 + self.set_download_action_sensitivity() + + if self.testing_auto_exit_trip_counter == self.testing_auto_exit_trip and self.testing_auto_exit: + self.on_rapidapp_destroy(self.rapidapp) else: - return (-99, None) - return (1, None) - - def extExifDateTime(self, name): - """Returns first extension, exif date time and subseconds data for the already downloaded image""" - return (self.images[name][0][0], self.images[name][1], self.images[name][2]) - -class TimeForDownload: - # used to store variables, see below - pass + if not self.testing_auto_exit and not self.auto_start_is_on: + self.download_progressbar.set_text(_("Thumbnails")) + self.thumbnails.generate_thumbnails(scan_pid) + elif self.auto_start_is_on: + if self.need_job_code_for_naming and not self.job_code: + self.get_job_code() + else: + self.start_download(scan_pid=scan_pid) -class TimeRemaining: - gap = 2 - def __init__(self): - self.clear() - - def set(self, w, size): - t = TimeForDownload() - t.timeRemaining = None - t.size = size - t.downloaded = 0 - t.sizeMark = 0 - t.timeMark = time.time() - self.times[w] = t - - def update(self, w, size): - if w in self.times: - self.times[w].downloaded += size - now = time.time() - tm = self.times[w].timeMark - amtTime = now - tm - if amtTime > self.gap: - self.times[w].timeMark = now - amtDownloaded = self.times[w].downloaded - self.times[w].sizeMark - self.times[w].sizeMark = self.times[w].downloaded - timefraction = amtDownloaded / float(amtTime) - amtToDownload = float(self.times[w].size) - self.times[w].downloaded - if timefraction: - self.times[w].timeRemaining = amtToDownload / timefraction - - def _timeEstimates(self): - for t in self.times: - yield self.times[t].timeRemaining + self.set_thumbnail_sort() - def timeRemaining(self): - return max(self._timeEstimates()) - - def setTimeMark(self, w): - if w in self.times: - self.times[w].timeMark = time.time() + # signal that no more data is coming, finishing io watch for this pipe + return False + else: + if len(data) > self.batch_size: + logger.critical("incoming pipe length is unexpectedly long: %s" % len(data)) + else: + for rpd_file in data: + self.thumbnails.add_file(rpd_file=rpd_file, + generate_thumbnail = not self.auto_start_is_on) - def clear(self): - self.times = {} + # must return True for this method to be called again + return True - def remove(self, w): - if w in self.times: - del self.times[w] -def programStatus(): - print _("Goodbye") + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='b') + def is_running (self): + return self.running + + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='') + def start (self): + if self.is_running(): + self.rapidapp.present() + else: + self.running = True + gtk.main() -def start (): - global is_beta - is_beta = config.version.find('~b') > 0 +def start(): + + is_beta = config.version.find('~') > 0 - parser = OptionParser(version= "%%prog %s" % config.version) + parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version)) parser.set_defaults(verbose=is_beta, extensions=False) # Translators: this text is displayed to the user when they request information on the command line options. # The text %default should not be modified or left out. @@ -6468,19 +3009,18 @@ def start (): parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list photo and video file extensions the program recognizes and exit")) parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit")) (options, args) = parser.parse_args() - global verbose - verbose = options.verbose - global debug_info - debug_info = options.debug - if debug_info: - verbose = True + if options.debug: + logging_level = logging.DEBUG + elif options.verbose: + logging_level = logging.INFO + else: + logging_level = logging.ERROR - if verbose: - atexit.register(programStatus) - + logger.setLevel(logging_level) + if options.extensions: - extensions = ((metadata.RAW_FILE_EXTENSIONS + metadata.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (videometadata.VIDEO_FILE_EXTENSIONS, _("Videos:"))) + extensions = ((rpdfile.RAW_FILE_EXTENSIONS + rpdfile.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_FILE_EXTENSIONS, _("Videos:"))) for exts, file_type in extensions: v = '' for e in exts[:-1]: @@ -6496,43 +3036,25 @@ def start (): print _("All settings and preferences have been reset") sys.exit(0) - cmd_line(_("Rapid Photo Downloader") + " %s" % config.version) - cmd_line(_("Using") + " pyexiv2 " + metadata.version_info()) - cmd_line(_("Using") + " exiv2 " + metadata.exiv2_version_info()) + logger.info("Rapid Photo Downloader %s", utilities.human_readable_version(config.version)) + logger.info("Using pyexiv2 %s", metadataphoto.pyexiv2_version_info()) + logger.info("Using exiv2 %s", metadataphoto.exiv2_version_info()) if DOWNLOAD_VIDEO: - cmd_line(_("Using") + " hachoir " + videometadata.version_info()) - else: - cmd_line(_("\n" + "Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.") + "\n") - - if using_gio: - cmd_line(_("Using") + " GIO") - gobject.threads_init() + logger.info("Using hachoir %s", metadatavideo.version_info()) else: - # Which volume management code is being used (GIO or GnomeVFS) - cmd_line(_("Using") + " GnomeVFS") - gdk.threads_init() - - + logger.info(_("Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.")) - display_queue.open("rw") - tube.tube_add_watch(display_queue, updateDisplay) - - gdk.threads_enter() - - # run only a single instance of the application bus = dbus.SessionBus () request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) - if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: - app = RapidApp (bus, '/', config.DBUS_NAME) + if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: + app = RapidApp(bus, '/', config.DBUS_NAME) else: # this application is already running - print _("%s is already running") % PROGRAM_NAME + print "Rapid Photo Downloader is already running" object = bus.get_object (config.DBUS_NAME, "/") app = dbus.Interface (object, config.DBUS_NAME) - app.start() - - gdk.threads_leave() + app.start() if __name__ == "__main__": start() diff --git a/rapid/renamesubfolderprefs.py b/rapid/renamesubfolderprefs.py deleted file mode 100644 index d538030..0000000 --- a/rapid/renamesubfolderprefs.py +++ /dev/null @@ -1,1695 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" Define and test preferences for use in PlusMinus tables. - -These are displayed to the user as a series of rows in the user -preferences dialog window. - -Preferences for subfolders and image renaming are defined below -in dictionaries and lists. This makes it easier for checking validity and -creating combo boxes. - -There are 3 levels: 0, 1 and 2, which specify the depth of the pref value. -Level 0 is the topmost level, and corresponds to the first entry in the -row of preferences the user sees in the preferences dialog window. - -Custom exceptions are defined to handle invalid preferences. - -The user's actual preferences, on the other hand, are stored in flat lists. -Each list has members which are a multiple of 3 in length. -Each group of 3 members is equal to one line of preferences in the plus minus -table. -""" -#needed for python 2.5, unneeded for python 2.6 -from __future__ import with_statement - -import string - -import os -import re -import sys - -import gtk.gdk as gdk - -try: - import pygtk - pygtk.require("2.0") -except: - pass -try: - import gtk -except: - sys.exit(1) - -from common import Configi18n -global _ -_ = Configi18n._ - -import datetime - -import config - -from common import pythonifyVersion -import problemnotification as pn - - -# Special key in each dictionary which specifies the order of elements. -# It is very important to have a consistent and rational order when displaying -# these prefs to the user, and dictionaries are unsorted. - -ORDER_KEY = "__order__" - -# PLEASE NOTE: these values are duplicated in a dummy class whose function -# is to have them put into the translation template. If you change the values below -# then you MUST change the value in class i18TranslateMeThanks as well!! - -# *** Level 0 -DATE_TIME = 'Date time' -TEXT = 'Text' -FILENAME = 'Filename' -METADATA = 'Metadata' -SEQUENCES = 'Sequences' -JOB_CODE = 'Job code' - -SEPARATOR = os.sep - -# *** Level 1 - -# Date time -IMAGE_DATE = 'Image date' -TODAY = 'Today' -YESTERDAY = 'Yesterday' -VIDEO_DATE = 'Video date' -DOWNLOAD_TIME = 'Download time' - -# File name -NAME_EXTENSION = 'Name + extension' -NAME = 'Name' -EXTENSION = 'Extension' -IMAGE_NUMBER = 'Image number' -VIDEO_NUMBER = 'Video number' - -# Metadata -APERTURE = 'Aperture' -ISO = 'ISO' -EXPOSURE_TIME = 'Exposure time' -FOCAL_LENGTH = 'Focal length' -CAMERA_MAKE = 'Camera make' -CAMERA_MODEL = 'Camera model' -SHORT_CAMERA_MODEL = 'Short camera model' -SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model' -SERIAL_NUMBER = 'Serial number' -SHUTTER_COUNT = 'Shutter count' -OWNER_NAME = 'Owner name' - -# Video metadata -CODEC = 'Codec' -WIDTH = 'Width' -HEIGHT = 'Height' -FPS = 'Frames Per Second' -LENGTH = 'Length' - -#Image sequences -DOWNLOAD_SEQ_NUMBER = 'Downloads today' -SESSION_SEQ_NUMBER = 'Session number' -SUBFOLDER_SEQ_NUMBER = 'Subfolder number' -STORED_SEQ_NUMBER = 'Stored number' - -SEQUENCE_LETTER = 'Sequence letter' - - - -# *** Level 2 - -# Image number -IMAGE_NUMBER_ALL = 'All digits' -IMAGE_NUMBER_1 = 'Last digit' -IMAGE_NUMBER_2 = 'Last 2 digits' -IMAGE_NUMBER_3 = 'Last 3 digits' -IMAGE_NUMBER_4 = 'Last 4 digits' - - -# Case -ORIGINAL_CASE = "Original Case" -UPPERCASE = "UPPERCASE" -LOWERCASE = "lowercase" - -# Sequence number -SEQUENCE_NUMBER_1 = "One digit" -SEQUENCE_NUMBER_2 = "Two digits" -SEQUENCE_NUMBER_3 = "Three digits" -SEQUENCE_NUMBER_4 = "Four digits" -SEQUENCE_NUMBER_5 = "Five digits" -SEQUENCE_NUMBER_6 = "Six digits" -SEQUENCE_NUMBER_7 = "Seven digits" - - -# Now, define dictionaries and lists of valid combinations of preferences. - -# Level 2 - -# Date - -SUBSECONDS = 'Subseconds' - -# ****** NOTE 1: if changing LIST_DATE_TIME_L2, you MUST update the default subfolder preference below ***** -# ****** NOTE 2: if changing LIST_DATE_TIME_L2, you MUST update DATE_TIME_CONVERT below ***** -LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', - 'MMDDYYYY', 'MMDDYY', 'MMDD', - 'DDMMYYYY', 'DDMMYY', 'YYYY', 'YY', - 'MM', 'DD', - 'HHMMSS', 'HHMM', 'HH-MM-SS', 'HH-MM', 'HH', 'MM (minutes)', 'SS'] - - -LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS] - -DEFAULT_SUBFOLDER_PREFS = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] -DEFAULT_VIDEO_SUBFOLDER_PREFS = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]] - -class i18TranslateMeThanks: - """ this class is never used in actual running code - It's purpose is to have these values inserted into the program's i18n template file - - """ - def __init__(self): - _('Date time') - _('Text') - _('Filename') - _('Metadata') - _('Sequences') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - _('Job code') - _('Image date') - _('Video date') - _('Today') - _('Yesterday') - # Translators: Download time is the time and date that the download started (when the user clicked the Download button) - _('Download time') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Name + extension') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Name') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Extension') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Image number') - _('Video number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Aperture') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('ISO') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Exposure time') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Focal length') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Camera make') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Camera model') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Short camera model') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Hyphenated short camera model') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Serial number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Shutter count') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Owner name') - _('Codec') - _('Width') - _('Height') - _('Length') - _('Frames Per Second') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Downloads today') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Session number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Subfolder number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Stored number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters - _('Sequence letter') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('All digits') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last digit') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last 2 digits') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last 3 digits') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last 4 digits') - # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization - _("Original Case") - # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization - _("UPPERCASE") - # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization - _("lowercase") - _("One digit") - _("Two digits") - _("Three digits") - _("Four digits") - _("Five digits") - _("Six digits") - _("Seven digits") - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('Subseconds') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYYYMMDD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYYY-MM-DD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYMMDD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YY-MM-DD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MMDDYYYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MMDDYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MMDD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('DDMMYYYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('DDMMYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MM') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('DD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HHMMSS') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HHMM') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HH-MM-SS') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HH-MM') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HH') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MM (minutes)') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('SS') - - -# Convenience values for python datetime conversion using values in -# LIST_DATE_TIME_L2. Obviously the two must remain synchronized. - -DATE_TIME_CONVERT = ['%Y%m%d', '%Y-%m-%d','%y%m%d', '%y-%m-%d', - '%m%d%Y', '%m%d%y', '%m%d', - '%d%m%Y', '%d%m%y', '%Y', '%y', - '%m', '%d', - '%H%M%S', '%H%M', '%H-%M-%S', '%H-%M', - '%H', '%M', '%S'] - - -LIST_IMAGE_NUMBER_L2 = [IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, - IMAGE_NUMBER_3, IMAGE_NUMBER_4] - - -LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE] - -LIST_SEQUENCE_LETTER_L2 = [ - UPPERCASE, - LOWERCASE - ] - - - -LIST_SEQUENCE_NUMBERS_L2 = [ - SEQUENCE_NUMBER_1, - SEQUENCE_NUMBER_2, - SEQUENCE_NUMBER_3, - SEQUENCE_NUMBER_4, - SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, - SEQUENCE_NUMBER_7, - ] - - - -LIST_SHUTTER_COUNT_L2 = [ - SEQUENCE_NUMBER_3, - SEQUENCE_NUMBER_4, - SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, - ] - -# Level 1 -LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] -LIST_VIDEO_DATE_TIME_L1 = [VIDEO_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] - -DICT_DATE_TIME_L1 = { - IMAGE_DATE: LIST_IMAGE_DATE_TIME_L2, - TODAY: LIST_DATE_TIME_L2, - YESTERDAY: LIST_DATE_TIME_L2, - DOWNLOAD_TIME: LIST_DATE_TIME_L2, - ORDER_KEY: LIST_DATE_TIME_L1 - } - -VIDEO_DICT_DATE_TIME_L1 = { - VIDEO_DATE: LIST_IMAGE_DATE_TIME_L2, - TODAY: LIST_DATE_TIME_L2, - YESTERDAY: LIST_DATE_TIME_L2, - DOWNLOAD_TIME: LIST_DATE_TIME_L2, - ORDER_KEY: LIST_VIDEO_DATE_TIME_L1 - } - - -LIST_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, IMAGE_NUMBER] - -DICT_FILENAME_L1 = { - NAME_EXTENSION: LIST_CASE_L2, - NAME: LIST_CASE_L2, - EXTENSION: LIST_CASE_L2, - IMAGE_NUMBER: LIST_IMAGE_NUMBER_L2, - ORDER_KEY: LIST_FILENAME_L1 - } - -LIST_VIDEO_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, VIDEO_NUMBER] - -DICT_VIDEO_FILENAME_L1 = { - NAME_EXTENSION: LIST_CASE_L2, - NAME: LIST_CASE_L2, - EXTENSION: LIST_CASE_L2, - VIDEO_NUMBER: LIST_IMAGE_NUMBER_L2, - ORDER_KEY: LIST_VIDEO_FILENAME_L1 - } - - -LIST_SUBFOLDER_FILENAME_L1 = [EXTENSION] - -DICT_SUBFOLDER_FILENAME_L1 = { - EXTENSION: LIST_CASE_L2, - ORDER_KEY: LIST_SUBFOLDER_FILENAME_L1 -} - -LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, - CAMERA_MAKE, CAMERA_MODEL, - SHORT_CAMERA_MODEL, - SHORT_CAMERA_MODEL_HYPHEN, - SERIAL_NUMBER, - SHUTTER_COUNT, - OWNER_NAME] - -LIST_VIDEO_METADATA_L1 = [CODEC, WIDTH, HEIGHT, LENGTH, FPS] - -DICT_METADATA_L1 = { - APERTURE: None, - ISO: None, - EXPOSURE_TIME: None, - FOCAL_LENGTH: None, - CAMERA_MAKE: LIST_CASE_L2, - CAMERA_MODEL: LIST_CASE_L2, - SHORT_CAMERA_MODEL: LIST_CASE_L2, - SHORT_CAMERA_MODEL_HYPHEN: LIST_CASE_L2, - SERIAL_NUMBER: None, - SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, - OWNER_NAME: LIST_CASE_L2, - ORDER_KEY: LIST_METADATA_L1 - } - -DICT_VIDEO_METADATA_L1 = { - CODEC: LIST_CASE_L2, - WIDTH: None, - HEIGHT: None, - LENGTH: None, - FPS: None, - ORDER_KEY: LIST_VIDEO_METADATA_L1 - } - -LIST_SEQUENCE_L1 = [ - DOWNLOAD_SEQ_NUMBER, - STORED_SEQ_NUMBER, - SESSION_SEQ_NUMBER, - SEQUENCE_LETTER - ] - -DICT_SEQUENCE_L1 = { - DOWNLOAD_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, - STORED_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, - SESSION_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, - SEQUENCE_LETTER: LIST_SEQUENCE_LETTER_L2, - ORDER_KEY: LIST_SEQUENCE_L1 - } - - -# Level 0 - - -LIST_IMAGE_RENAME_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, - SEQUENCES, JOB_CODE] - -LIST_VIDEO_RENAME_L0 = LIST_IMAGE_RENAME_L0 - - -DICT_IMAGE_RENAME_L0 = { - DATE_TIME: DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_FILENAME_L1, - METADATA: DICT_METADATA_L1, - SEQUENCES: DICT_SEQUENCE_L1, - JOB_CODE: None, - ORDER_KEY: LIST_IMAGE_RENAME_L0 - } - -DICT_VIDEO_RENAME_L0 = { - DATE_TIME: VIDEO_DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_VIDEO_FILENAME_L1, - METADATA: DICT_VIDEO_METADATA_L1, - SEQUENCES: DICT_SEQUENCE_L1, - JOB_CODE: None, - ORDER_KEY: LIST_VIDEO_RENAME_L0 - } - -LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] - -DICT_SUBFOLDER_L0 = { - DATE_TIME: DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_SUBFOLDER_FILENAME_L1, - METADATA: DICT_METADATA_L1, - JOB_CODE: None, - SEPARATOR: None, - ORDER_KEY: LIST_SUBFOLDER_L0 - } - -LIST_VIDEO_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] - -DICT_VIDEO_SUBFOLDER_L0 = { - DATE_TIME: VIDEO_DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_SUBFOLDER_FILENAME_L1, - METADATA: DICT_VIDEO_METADATA_L1, - JOB_CODE: None, - SEPARATOR: None, - ORDER_KEY: LIST_VIDEO_SUBFOLDER_L0 - } - -# preference elements that require metadata -# note there is no need to specify lower level elements if a higher level -# element is necessary for them to be present to begin with -METADATA_ELEMENTS = [METADATA, IMAGE_DATE] - -# preference elements that are sequence numbers or letters -SEQUENCE_ELEMENTS = [ - DOWNLOAD_SEQ_NUMBER, - SESSION_SEQ_NUMBER, - SUBFOLDER_SEQ_NUMBER, - STORED_SEQ_NUMBER, - SEQUENCE_LETTER] - -# preference elements that do not require metadata and are not fixed -# as above, there is no need to specify lower level elements if a higher level -# element is necessary for them to be present to begin with -DYNAMIC_NON_METADATA_ELEMENTS = [ - TODAY, YESTERDAY, - FILENAME] + SEQUENCE_ELEMENTS - - - -#the following is what the preferences looked in older versions of the program -#they are here for reference, and for checking the validity of preferences - -USER_INPUT = 'User' - -DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7 = 'Downloads today' -SESSION_SEQ_NUMBER_V_0_0_8_B7 = 'Session sequence number' -SUBFOLDER_SEQ_NUMBER_V_0_0_8_B7 = 'Subfolder sequence number' -STORED_SEQ_NUMBER_V_0_0_8_B7 = 'Stored sequence number' -SEQUENCE_LETTER_V_0_0_8_B7 = 'Sequence letter' - -LIST_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 = [ - SEQUENCE_NUMBER_1, - SEQUENCE_NUMBER_2, - SEQUENCE_NUMBER_3, - SEQUENCE_NUMBER_4, - SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, - ] - -DICT_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 = { - SEQUENCE_NUMBER_1: None, - SEQUENCE_NUMBER_2: None, - SEQUENCE_NUMBER_3: None, - SEQUENCE_NUMBER_4: None, - SEQUENCE_NUMBER_5: None, - SEQUENCE_NUMBER_6: None, - ORDER_KEY: LIST_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 - } - -LIST_SEQUENCE_L1_V_0_0_8_B7 = [USER_INPUT] - -DICT_SEQUENCE_L1_V_0_0_8_B7 = { - USER_INPUT: DICT_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7, - ORDER_KEY: LIST_SEQUENCE_L1_V_0_0_8_B7 - } - -LIST_SEQUENCE_LETTER_L1_L1_V_0_0_8_B7 = [ - UPPERCASE, - LOWERCASE - ] - -DICT_SEQUENCE_LETTER_L1_V_0_0_8_B7 = { - UPPERCASE: None, - LOWERCASE: None, - ORDER_KEY: LIST_SEQUENCE_LETTER_L1_L1_V_0_0_8_B7 - } - -LIST_IMAGE_RENAME_L0_V_0_0_8_B7 = [DATE_TIME, TEXT, FILENAME, METADATA, - DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7, - SESSION_SEQ_NUMBER_V_0_0_8_B7, - SEQUENCE_LETTER_V_0_0_8_B7] - -DICT_IMAGE_RENAME_L0_V_0_0_8_B7 = { - DATE_TIME: DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_FILENAME_L1, - METADATA: DICT_METADATA_L1, - DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7: None, - SESSION_SEQ_NUMBER_V_0_0_8_B7: None, - SEQUENCE_LETTER_V_0_0_8_B7: DICT_SEQUENCE_LETTER_L1_V_0_0_8_B7, - ORDER_KEY: LIST_IMAGE_RENAME_L0_V_0_0_8_B7 - } - -PREVIOUS_IMAGE_RENAME= { - '0.0.8~b7': DICT_IMAGE_RENAME_L0_V_0_0_8_B7, - } - - -# Functions to work with above data - -def _getPrevPrefs(oldDefs, currentDefs, previousVersion): - k = oldDefs.keys() - # if there were other defns, we'd need to figure out which one - # but currently, there are no others - # there will be in future, and this code wil be updated then - version_change = pythonifyVersion(k[0]) - if pythonifyVersion(previousVersion) <= version_change: - return oldDefs[k[0]] - else: - return currentDefs - -def _upgradePreferencesToCurrent(prefs, previousVersion): - """ checks to see if preferences should be upgraded - - returns True if they were upgraded, and the new prefs - - VERY IMPORTANT: the new prefs will be a new list, not an inplace - modification of the existing preferences! Otherwise, the check on - assignment in the prefs.py __setattr__ will not work as expected!! - """ - upgraded = False - # code to upgrade from <= 0.0.8~b7 to >= 0.0.8~b8 - p = [] - for i in range(0, len(prefs), 3): - if prefs[i] in [SEQUENCE_LETTER_V_0_0_8_B7, SESSION_SEQ_NUMBER_V_0_0_8_B7]: - upgraded = True - p.append(SEQUENCES) - if prefs[i] == SEQUENCE_LETTER_V_0_0_8_B7: - p.append(SEQUENCE_LETTER) - p.append(prefs[i+1]) - else: - p.append(SESSION_SEQ_NUMBER) - p.append(prefs[i+2]) - else: - p += prefs[i:i+3] - - assert(len(prefs)==len(p)) - return (upgraded, p) - - -def upgradePreferencesToCurrent(imageRenamePrefs, subfolderPrefs, previousVersion): - """Upgrades user preferences to current version - - returns True if the preferences were upgraded""" - - # only check image rename, for now.... - upgraded, imageRenamePrefs = _upgradePreferencesToCurrent(imageRenamePrefs, previousVersion) - return (upgraded, imageRenamePrefs , subfolderPrefs) - - -def usesJobCode(prefs): - """ Returns True if the preferences contain a job code, else returns False""" - for i in range(0, len(prefs), 3): - if prefs[i] == JOB_CODE: - return True - return False - -def checkPreferencesForValidity(imageRenamePrefs, subfolderPrefs, videoRenamePrefs, videoSubfolderPrefs, version=config.version): - """ - Checks preferences for validity (called at program startup) - - Returns true if the passed in preferences are valid, else returns False - """ - - if version == config.version: - try: - tests = ((imageRenamePrefs, ImageRenamePreferences), - (subfolderPrefs, SubfolderPreferences), - (videoRenamePrefs, VideoRenamePreferences), - (videoSubfolderPrefs, VideoSubfolderPreferences)) - for i, Prefs in tests: - p = Prefs(i, None) - p.checkPrefsForValidity() - except: - return False - return True - else: - defn = _getPrevPrefs(PREVIOUS_IMAGE_RENAME, DICT_IMAGE_RENAME_L0, version) - try: - checkPreferenceValid(defn, imageRenamePrefs) - checkPreferenceValid(DICT_SUBFOLDER_L0, subfolderPrefs) - checkPreferenceValid(DICT_VIDEO_SUBFOLDER_L0, videoSubfolderPrefs) - checkPreferenceValid(DICT_VIDEO_RENAME_L0, videoRenamePrefs) - except: - return False - return True - -def checkPreferenceValid(prefDefinition, prefs, modulo=3): - """ - Checks to see if prefs are valid according to definition. - - prefs is a list of preferences. - prefDefinition is a Dict specifying what is valid. - modulo is how many list elements are equivalent to one line of preferences. - - Returns True if prefs match with prefDefinition, - else raises appropriate error. - """ - - if (len(prefs) % modulo <> 0) or not prefs: - raise PrefLengthError(prefs) - else: - for i in range(0, len(prefs), modulo): - _checkPreferenceValid(prefDefinition, prefs[i:i+modulo]) - - return True - -def _checkPreferenceValid(prefDefinition, prefs): - - key = prefs[0] - value = prefs[1] - - - if prefDefinition.has_key(key): - - nextPrefDefinition = prefDefinition[key] - - if value == None: - # value should never be None, at any time - raise PrefValueInvalidError((None, nextPrefDefinition)) - - if nextPrefDefinition and not value: - raise PrefValueInvalidError((value, nextPrefDefinition)) - - if type(nextPrefDefinition) == type({}): - return _checkPreferenceValid(nextPrefDefinition, prefs[1:]) - else: - if type(nextPrefDefinition) == type([]): - result = value in nextPrefDefinition - if not result: - raise PrefValueInvalidError((value, nextPrefDefinition)) - return True - elif not nextPrefDefinition: - return True - else: - result = nextPrefDefinition == value - if not result: - raise PrefKeyValue((value, nextPrefDefinition)) - return True - else: - raise PrefKeyError((key, prefDefinition[ORDER_KEY])) - -def filterSubfolderPreferences(prefList): - """ - Filters out extraneous preference choices - """ - prefs_changed = False - continueCheck = True - while continueCheck and prefList: - continueCheck = False - if prefList[0] == SEPARATOR: - # Subfolder preferences should not start with a / - prefList = prefList[3:] - prefs_changed = True - continueCheck = True - elif prefList[-3] == SEPARATOR: - # Subfolder preferences should not end with a / - prefList = prefList[:-3] - continueCheck = True - prefs_changed = True - else: - for i in range(0, len(prefList) - 3, 3): - if prefList[i] == SEPARATOR and prefList[i+3] == SEPARATOR: - # Subfolder preferences should not contain two /s side by side - continueCheck = True - prefs_changed = True - # note we are messing with the contents of the pref list, - # must exit loop and try again - prefList = prefList[:i] + prefList[i+3:] - break - - return (prefs_changed, prefList) - - -class PrefError(Exception): - """ base class """ - def unpackList(self, l): - """ - Make the preferences presentable to the user - """ - - s = '' - for i in l: - if i <> ORDER_KEY: - s += "'" + i + "', " - return s[:-2] - - def __str__(self): - return self.msg - -class PrefKeyError(PrefError): - def __init__(self, error): - value = error[0] - expectedValues = self.unpackList(error[1]) - self.msg = _("Preference key '%(key)s' is invalid.\nExpected one of %(value)s") % { - 'key': value, 'value': expectedValues} - - -class PrefValueInvalidError(PrefKeyError): - def __init__(self, error): - value = error[0] - self.msg = _("Preference value '%(value)s' is invalid") % {'value': value} - -class PrefLengthError(PrefError): - def __init__(self, error): - self.msg = _("These preferences are not well formed:") + "\n %s" % self.unpackList(error) - -class PrefValueKeyComboError(PrefError): - def __init__(self, error): - self.msg = error - - -def convertDateForStrftime(dateTimeUserChoice): - try: - return DATE_TIME_CONVERT[LIST_DATE_TIME_L2.index(dateTimeUserChoice)] - except: - raise PrefValueInvalidError(dateTimeUserChoice) - - -class Comboi18n(gtk.ComboBox): - """ very simple i18n version of the venerable combo box - with one column displayed to the user. - - This combo box has two columns: - 1. the first contains the actual value and is invisible - 2. the second contains the translation of the first column, and this is what - the users sees - """ - def __init__(self): - liststore = gtk.ListStore(str, str) - gtk.ComboBox.__init__(self, liststore) - cell = gtk.CellRendererText() - self.pack_start(cell, True) - self.add_attribute(cell, 'text', 1) - # must name the combo box on pygtk used in Ubuntu 11.04, Fedora 15, etc. - self.set_name('GtkComboBox') - - def append_text(self, text): - model = self.get_model() - model.append((text, _(text))) - - def get_active_text(self): - model = self.get_model() - active = self.get_active() - if active < 0: - return None - return model[active][0] - -class ImageRenamePreferences: - def __init__(self, prefList, parent, fileSequenceLock=None, sequences=None): - """ - Exception raised if preferences are invalid. - - This should be caught by calling class.""" - - self.parent = parent - self.prefList = prefList - - # use variables for determining sequence numbers - # there are two possibilities: - # 1. this code is being called while being run from within a copy photos process - # 2. it's being called from within the preferences dialog window - - self.fileSequenceLock = fileSequenceLock - self.sequences = sequences - - self.job_code = '' - - # derived classes will have their own definitions, do not overwrite - if not hasattr(self, "prefsDefnL0"): - self.prefsDefnL0 = DICT_IMAGE_RENAME_L0 - self.defaultPrefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] - self.defaultRow = self.defaultPrefs - self.stripForwardSlash = True - self.L1DateCheck = IMAGE_DATE #used in _getDateComponent() - self.component = pn.FILENAME_COMPONENT - - - def initializeProblem(self, problem): - """ - Set the problem tracker used in name generation - """ - self.problem = problem - - def getProblems(self): - """ - Returns Problem class if there were problems, else returns None. - """ - if self.problem.has_problem(): - return self.problem - else: - return None - - def checkPrefsForValidity(self): - """ - Checks image preferences validity - """ - - return checkPreferenceValid(self.prefsDefnL0, self.prefList) - - def formatPreferencesForPrettyPrint(self): - """ returns a string useful for printing the preferences""" - - v = '' - - for i in range(0, len(self.prefList), 3): - if (self.prefList[i+1] or self.prefList[i+2]): - c = ':' - else: - c = '' - s = "%s%s " % (self.prefList[i], c) - - if self.prefList[i+1]: - s = "%s%s" % (s, self.prefList[i+1]) - if self.prefList[i+2]: - s = "%s (%s)" % (s, self.prefList[i+2]) - - v += s + "\n" - return v - - - def setJobCode(self, job_code): - self.job_code = job_code - - def setDownloadStartTime(self, download_start_time): - self.download_start_time = download_start_time - - def _getDateComponent(self): - """ - Returns portion of new image / subfolder name based on date time. - If the date is missing, will attempt to use the fallback date. - """ - - # step 1: get the correct value from metadata - if self.L1 == self.L1DateCheck: - if self.L2 == SUBSECONDS: - d = self.metadata.subSeconds() - if d == '00': - self.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L2)) - return '' - else: - return d - else: - d = self.metadata.dateTime(missing=None) - - elif self.L1 == TODAY: - d = datetime.datetime.now() - elif self.L1 == YESTERDAY: - delta = datetime.timedelta(days = 1) - d = datetime.datetime.now() - delta - elif self.L1 == DOWNLOAD_TIME: - d = self.download_start_time - else: - raise("Date options invalid") - - # step 2: handle a missing value - if not d: - if self.fallback_date: - try: - d = datetime.datetime.fromtimestamp(self.fallback_date) - except: - self.problem.add_problem(self.component, pn.INVALID_DATE_TIME, '') - return '' - else: - self.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) - return '' - - try: - return d.strftime(convertDateForStrftime(self.L2)) - except: - self.problem.add_problem(self.component, pn.INVALID_DATE_TIME, d) - return '' - - def _getFilenameComponent(self): - """ - Returns portion of new image / subfolder name based on the file name - """ - - name, extension = os.path.splitext(self.existingFilename) - - if self.L1 == NAME_EXTENSION: - filename = self.existingFilename - elif self.L1 == NAME: - filename = name - elif self.L1 == EXTENSION: - if extension: - if not self.stripInitialPeriodFromExtension: - # keep the period / dot of the extension, so the user does not - # need to manually specify it - filename = extension - else: - # having the period when this is used as a part of a subfolder name - # is a bad idea! - filename = extension[1:] - else: - self.problem.add_problem(self.component, pn.MISSING_FILE_EXTENSION) - return "" - elif self.L1 == IMAGE_NUMBER or self.L1 == VIDEO_NUMBER: - n = re.search("(?P<image_number>[0-9]+$)", name) - if not n: - self.problem.add_problem(self.component, pn.MISSING_IMAGE_NUMBER) - return '' - else: - image_number = n.group("image_number") - - if self.L2 == IMAGE_NUMBER_ALL: - filename = image_number - elif self.L2 == IMAGE_NUMBER_1: - filename = image_number[-1] - elif self.L2 == IMAGE_NUMBER_2: - filename = image_number[-2:] - elif self.L2 == IMAGE_NUMBER_3: - filename = image_number[-3:] - elif self.L2 == IMAGE_NUMBER_4: - filename = image_number[-4:] - else: - raise TypeError("Incorrect filename option") - - if self.L2 == UPPERCASE: - filename = filename.upper() - elif self.L2 == LOWERCASE: - filename = filename.lower() - - return filename - - def _getMetadataComponent(self): - """ - Returns portion of new image / subfolder name based on the metadata - - Note: date time metadata found in _getDateComponent() - """ - - if self.L1 == APERTURE: - v = self.metadata.aperture() - elif self.L1 == ISO: - v = self.metadata.iso() - elif self.L1 == EXPOSURE_TIME: - v = self.metadata.exposureTime(alternativeFormat=True) - elif self.L1 == FOCAL_LENGTH: - v = self.metadata.focalLength() - elif self.L1 == CAMERA_MAKE: - v = self.metadata.cameraMake() - elif self.L1 == CAMERA_MODEL: - v = self.metadata.cameraModel() - elif self.L1 == SHORT_CAMERA_MODEL: - v = self.metadata.shortCameraModel() - elif self.L1 == SHORT_CAMERA_MODEL_HYPHEN: - v = self.metadata.shortCameraModel(includeCharacters = "\-") - elif self.L1 == SERIAL_NUMBER: - v = self.metadata.cameraSerial() - elif self.L1 == SHUTTER_COUNT: - v = self.metadata.shutterCount() - if v: - v = int(v) - padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3 - formatter = '%0' + str(padding) + "i" - v = formatter % v - - elif self.L1 == OWNER_NAME: - v = self.metadata.ownerName() - else: - raise TypeError("Invalid metadata option specified") - if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, - SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME]: - if self.L2 == UPPERCASE: - v = v.upper() - elif self.L2 == LOWERCASE: - v = v.lower() - if not v: - self.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) - return v - - - def _formatSequenceNo(self, value, amountToPad): - padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1 - formatter = '%0' + str(padding) + "i" - return formatter % value - - - def _calculateLetterSequence(self, sequence): - - def _letters(x): - """ - Adapted from algorithm at http://en.wikipedia.org/wiki/Hexavigesimal - """ - v = '' - while x > 25: - r = x % 26 - x= x / 26 - 1 - v = string.lowercase[r] + v - v = string.lowercase[x] + v - - return v - - - v = _letters(sequence) - if self.L2 == UPPERCASE: - v = v.upper() - - return v - - def _getSubfolderSequenceNo(self): - """ - Add a sequence number to the filename - - * Sequence numbering is per subfolder - * Assume the user might actually have a (perhaps odd) reason to have more - than one subfolder sequence number in the same file name - """ - - self.subfolderSeqNoInstanceInFilename += 1 - - if self.downloadSubfolder: - subfolder = self.downloadSubfolder + str(self.subfolderSeqNoInstanceInFilename) - else: - subfolder = "__subfolder__" + str(self.subfolderSeqNoInstanceInFilename) - - if self.fileSequenceLock: - with self.fileSequenceLock: - v = self.sequenceNos.calculate(subfolder) - v = self.formatSequenceNo(v, self.L1) - else: - v = self.sequenceNos.calculate(subfolder) - v = self.formatSequenceNo(v, self.L1) - - return v - - def _getSessionSequenceNo(self): - return self._formatSequenceNo(self.sequences.getSessionSequenceNoUsingCounter(self.sequenceCounter), self.L2) - - def _getDownloadsTodaySequenceNo(self): - return self._formatSequenceNo(self.sequences.getDownloadsTodayUsingCounter(self.sequenceCounter), self.L2) - - - def _getStoredSequenceNo(self): - return self._formatSequenceNo(self.sequences.getStoredSequenceNoUsingCounter(self.sequenceCounter), self.L2) - - def _getSequenceLetter(self): - return self._calculateLetterSequence(self.sequences.getSequenceLetterUsingCounter(self.sequenceCounter)) - - - def _getSequencesComponent(self): - if self.L1 == DOWNLOAD_SEQ_NUMBER: - return self._getDownloadsTodaySequenceNo() - elif self.L1 == SESSION_SEQ_NUMBER: - return self._getSessionSequenceNo() - elif self.L1 == SUBFOLDER_SEQ_NUMBER: - return self._getSubfolderSequenceNo() - elif self.L1 == STORED_SEQ_NUMBER: - return self._getStoredSequenceNo() - elif self.L1 == SEQUENCE_LETTER: - return self._getSequenceLetter() - - def _getComponent(self): - try: - if self.L0 == DATE_TIME: - return self._getDateComponent() - elif self.L0 == TEXT: - return self.L1 - elif self.L0 == FILENAME: - return self._getFilenameComponent() - elif self.L0 == METADATA: - return self._getMetadataComponent() - elif self.L0 == SEQUENCES: - return self._getSequencesComponent() - elif self.L0 == JOB_CODE: - return self.job_code - elif self.L0 == SEPARATOR: - return os.sep - except: - self.problem.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0)) - return '' - - def _getValuesFromList(self): - for i in range(0, len(self.prefList), 3): - yield (self.prefList[i], self.prefList[i+1], self.prefList[i+2]) - - - def _generateName(self, metadata, existingFilename, stripCharacters, subfolder, stripInitialPeriodFromExtension, sequence, fallback_date): - self.metadata = metadata - self.existingFilename = existingFilename - self.stripInitialPeriodFromExtension = stripInitialPeriodFromExtension - self.fallback_date = fallback_date - - name = '' - - #the subfolder in which the image will be downloaded to - self.downloadSubfolder = subfolder - - self.sequenceCounter = sequence - - for self.L0, self.L1, self.L2 in self._getValuesFromList(): - v = self._getComponent() - if v: - name += v - - if stripCharacters: - for c in r'\:*?"<>|': - name = name.replace(c, '') - - if self.stripForwardSlash: - name = name.replace('/', '') - - name = name.strip() - - return name - - def generateNameUsingPreferences(self, metadata, existingFilename=None, - stripCharacters = False, subfolder=None, - stripInitialPeriodFromExtension=False, - sequencesPreliminary = True, - sequence_to_use = None, - fallback_date = None): - """ - Generate a filename for the photo or video in string format based on user preferences. - - Returns the name in string format - - Any problems encountered during the generation of the name can be accessed - through the method getProblems() - """ - - if self.sequences: - if sequence_to_use is not None: - sequence = sequence_to_use - elif sequencesPreliminary: - sequence = self.sequences.getPrelimSequence() - else: - sequence = self.sequences.getFinalSequence() - else: - sequence = 0 - - return self._generateName(metadata, existingFilename, stripCharacters, subfolder, - stripInitialPeriodFromExtension, sequence, fallback_date) - - def generateNameSequencePossibilities(self, metadata, existingFilename, - stripCharacters=False, subfolder=None, - stripInitialPeriodFromExtension=False): - - """ Generates the possible image names using the sequence numbers / letter possibilities""" - - for sequence in self.sequences.getSequencePossibilities(): - yield self._generateName(metadata, existingFilename, stripCharacters, subfolder, - stripInitialPeriodFromExtension, sequence) - - def filterPreferences(self): - """ - Filters out extraneous preference choices - Expected to be implemented in derived classes when needed - """ - pass - - def needImageMetaDataToCreateUniqueName(self): - """ - Returns True if an image's metadata is essential to properly generate a unique image name - - Image names should be unique. Some images may not have metadata. If - only non-dynamic components make up the rest of an image name - (e.g. text specified by the user), then relying on metadata will likely - produce duplicate names. - - File extensions are not considered dynamic. - - This is NOT a general test to see if unique filenames can be generated. It is a test - to see if an image's metadata is needed. - """ - hasMD = hasDynamic = False - - for e in METADATA_ELEMENTS: - if e in self.prefList: - hasMD = True - break - - if hasMD: - for e in DYNAMIC_NON_METADATA_ELEMENTS: - if e in self.prefList: - if e == FILENAME and (NAME_EXTENSION in self.prefList or - NAME in self.prefList or - IMAGE_NUMBER in self.prefList): - hasDynamic = True - break - - if hasMD and not hasDynamic: - return True - else: - return False - - def usesSequenceElements(self): - """ Returns true if any sequence numbers or letters are used to generate the filename """ - - for e in SEQUENCE_ELEMENTS: - if e in self.prefList: - return True - - return False - - def usesTheSequenceElement(self, e): - """ Returns true if a stored sequence number is used to generate the filename """ - return e in self.prefList - - - def _createCombo(self, choices): - combobox = Comboi18n() - for text in choices: - combobox.append_text(text) - return combobox - - def getDefaultRow(self): - """ - returns a list of default widgets - """ - return self.getWidgetsBasedOnUserSelection(self.defaultRow) - - def _getPreferenceWidgets(self, prefDefinition, prefs, widgets): - key = prefs[0] - value = prefs[1] - - # supply a default value if the user has not yet chosen a value! - if not key: - key = prefDefinition[ORDER_KEY][0] - - if not key in prefDefinition: - raise PrefKeyError((key, prefDefinition.keys())) - - - list0 = prefDefinition[ORDER_KEY] - - # the first widget will always be a combo box - widget0 = self._createCombo(list0) - widget0.set_active(list0.index(key)) - - widgets.append(widget0) - - if key == TEXT: - widget1 = gtk.Entry() - widget1.set_text(value) - - widgets.append(widget1) - widgets.append(None) - return - elif key in [SEPARATOR, JOB_CODE]: - widgets.append(None) - widgets.append(None) - return - else: - nextPrefDefinition = prefDefinition[key] - if type(nextPrefDefinition) == type({}): - return self._getPreferenceWidgets(nextPrefDefinition, - prefs[1:], - widgets) - else: - if type(nextPrefDefinition) == type([]): - widget1 = self._createCombo(nextPrefDefinition) - if not value: - value = nextPrefDefinition[0] - try: - widget1.set_active(nextPrefDefinition.index(value)) - except: - raise PrefValueInvalidError((value, nextPrefDefinition)) - - widgets.append(widget1) - else: - widgets.append(None) - - def getWidgetsBasedOnPreferences(self): - """ - Yields a list of widgets and their callbacks based on the users preferences. - - This list is equivalent to one row of preferences when presented to the - user in the Plus Minus Table. - """ - - for L0, L1, L2 in self._getValuesFromList(): - prefs = [L0, L1, L2] - widgets = [] - self._getPreferenceWidgets(self.prefsDefnL0, prefs, widgets) - yield widgets - - - def getWidgetsBasedOnUserSelection(self, selection): - """ - Returns a list of widgets and their callbacks based on what the user has selected. - - Selection is the values the user has chosen thus far in comboboxes. - It determines the contents of the widgets returned. - It should be a list of three values, with None for values not chosen. - For values which are None, the first value in the preferences - definition is chosen. - - """ - widgets = [] - - self._getPreferenceWidgets(self.prefsDefnL0, selection, widgets) - return widgets - -def getVideoMetadataComponent(video): - """ - Returns portion of video / subfolder name based on the metadata - - This is outside of a class definition because of the inheritence - hierarchy. - """ - - problem = None - if video.L1 == CODEC: - v = video.metadata.codec() - elif video.L1 == WIDTH: - v = video.metadata.width() - elif video.L1 == HEIGHT: - v = video.metadata.height() - elif video.L1 == FPS: - v = video.metadata.framesPerSecond() - elif video.L1 == LENGTH: - v = video.metadata.length() - else: - raise TypeError("Invalid metadata option specified") - if video.L1 in [CODEC]: - if video.L2 == UPPERCASE: - v = v.upper() - elif video.L2 == LOWERCASE: - v = v.lower() - if not v: - video.problem.add_problem(video.component, pn.MISSING_METADATA, _(video.L1)) - return v - -class VideoRenamePreferences(ImageRenamePreferences): - def __init__(self, prefList, parent, fileSequenceLock=None, sequences=None): - self.prefsDefnL0 = DICT_VIDEO_RENAME_L0 - self.defaultPrefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] - self.defaultRow = self.defaultPrefs - self.stripForwardSlash = True - self.L1DateCheck = VIDEO_DATE - self.component = pn.FILENAME_COMPONENT - ImageRenamePreferences.__init__(self, prefList, parent, fileSequenceLock, sequences) - - def _getMetadataComponent(self): - """ - Returns portion of video / subfolder name based on the metadata - - Note: date time metadata found in _getDateComponent() - """ - return getVideoMetadataComponent(self) - - -class SubfolderPreferences(ImageRenamePreferences): - def __init__(self, prefList, parent): - self.prefsDefnL0 = DICT_SUBFOLDER_L0 - self.defaultPrefs = DEFAULT_SUBFOLDER_PREFS - self.defaultRow = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] - self.stripForwardSlash = False - self.L1DateCheck = IMAGE_DATE - self.component = pn.SUBFOLDER_COMPONENT - ImageRenamePreferences.__init__(self, prefList, parent) - - self.stripExtraneousWhiteSpace = re.compile(r'\s*%s\s*' % os.sep) - - def generateNameUsingPreferences(self, photo, existingFilename=None, - stripCharacters = False, fallback_date = None): - """ - Generate a filename for the photo in string format based on user prefs. - - Returns a tuple of two strings: - - the name - - any problems generating the name. If blank, there were no problems - """ - - subfolders = ImageRenamePreferences.generateNameUsingPreferences( - self, photo, - existingFilename, stripCharacters, - stripInitialPeriodFromExtension=True, - fallback_date=fallback_date) - # subfolder value must never start with a separator, or else any - # os.path.join function call will fail to join a subfolder to its - # parent folder - if subfolders: - if subfolders[0] == os.sep: - subfolders = subfolders[1:] - - # remove any spaces before and after a directory name - if subfolders and stripCharacters: - subfolders = self.stripExtraneousWhiteSpace.sub(os.sep, subfolders) - - return subfolders - - def filterPreferences(self): - filtered, prefList = filterSubfolderPreferences(self.prefList) - if filtered: - self.prefList = prefList - - def needMetaDataToCreateUniqueName(self): - """ - Returns True if metadata is essential to properly generate subfolders - - This will be the case if the only components are metadata and separators - """ - - for e in self.prefList: - if (not e) and ((e not in METADATA_ELEMENTS) or (e <> SEPARATOR)): - return True - - return False - - - - - def checkPrefsForValidity(self): - """ - Checks subfolder preferences validity above and beyond image name checks. - - See parent method for full description. - - Subfolders have additional requirments to that of image names. - """ - v = ImageRenamePreferences.checkPrefsForValidity(self) - if v: - # peform additional checks: - # 1. do not start with a separator - # 2. do not end with a separator - # 3. do not have two separators in a row - # these three rules will ensure something else other than a - # separator is specified - L1s = [] - for i in range(0, len(self.prefList), 3): - L1s.append(self.prefList[i]) - - if L1s[0] == SEPARATOR: - raise PrefValueKeyComboError(_("Subfolder preferences should not start with a %s") % os.sep) - elif L1s[-1] == SEPARATOR: - raise PrefValueKeyComboError(_("Subfolder preferences should not end with a %s") % os.sep) - else: - for i in range(len(L1s) - 1): - if L1s[i] == SEPARATOR and L1s[i+1] == SEPARATOR: - raise PrefValueKeyComboError(_("Subfolder preferences should not contain two %s one after the other") % os.sep) - return v - - - -class VideoSubfolderPreferences(SubfolderPreferences): - def __init__(self, prefList, parent): - SubfolderPreferences.__init__(self, prefList, parent) - self.prefsDefnL0 = DICT_VIDEO_SUBFOLDER_L0 - self.defaultPrefs = DEFAULT_VIDEO_SUBFOLDER_PREFS - self.defaultRow = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]] - self.L1DateCheck = VIDEO_DATE - self.component = pn.SUBFOLDER_COMPONENT - - def _getMetadataComponent(self): - """ - Returns portion of video / subfolder name based on the metadata - - Note: date time metadata found in _getDateComponent() - """ - return getVideoMetadataComponent(self) - -class Sequences: - """ - Holds sequence numbers and letters used in generating filenames. - The same instance of this class is shared among all threads. - """ - def __init__(self, downloadsToday, storedSequenceNo): - self.subfolderSequenceNo = {} - self.sessionSequenceNo = 1 - self.sequenceLetter = 0 - - self.setUseOfSequenceElements(False, False) - - self.assignedSequenceCounter = 1 - self.reset(downloadsToday, storedSequenceNo) - - def setUseOfSequenceElements(self, usesSessionSequenceNo, usesSequenceLetter): - self.usesSessionSequenceNo = usesSessionSequenceNo - self.usesSequenceLetter = usesSequenceLetter - - def reset(self, downloadsToday, storedSequenceNo): - self.downloadsToday = downloadsToday - self.downloadsTodayOffset = 0 - self.storedSequenceNo = storedSequenceNo - if self.usesSessionSequenceNo: - self.sessionSequenceNo = self.sessionSequenceNo + self.assignedSequenceCounter - 1 - if self.usesSequenceLetter: - self.sequenceLetter = self.sequenceLetter + self.assignedSequenceCounter - 1 - self.doNotAddToPool = False - self.pool = [] - self.poolSequenceCounter = 0 - self.assignedSequenceCounter = 1 - - def getPrelimSequence(self): - if self.doNotAddToPool: - self.doNotAddToPool = False - else: - # increment pool sequence number - self.poolSequenceCounter += 1 - self.pool.append(self.poolSequenceCounter) - - return self.poolSequenceCounter - - def getFinalSequence(self): - # get oldest queue value - # remove from queue or flag it should be removed - - return self.assignedSequenceCounter - - def getSequencePossibilities(self): - for i in self.pool: - yield i - - def getSessionSequenceNo(self): - return self.sessionSequenceNo + self.assignedSequenceCounter - 1 - - def getSessionSequenceNoUsingCounter(self, counter): - return self.sessionSequenceNo + counter - 1 - - def setSessionSequenceNo(self, value): - self.sessionSequenceNo = value - - def setStoredSequenceNo(self, value): - self.storedSequenceNo = value - - def getDownloadsTodayUsingCounter(self, counter): - return self.downloadsToday + counter - self.downloadsTodayOffset - - def setDownloadsToday(self, value): - self.downloadsToday = value - self.downloadsTodayOffset = self.assignedSequenceCounter - 1 - - def getStoredSequenceNoUsingCounter(self, counter): - return self.storedSequenceNo + counter - - def getSequenceLetterUsingCounter(self, counter): - return self.sequenceLetter + counter - 1 - - def imageCopyFailed(self): - self.doNotAddToPool = True - - def imageCopySucceeded(self): - self.increment() - - def increment(self): - assert(self.assignedSequenceCounter == self.pool[0]) - self.assignedSequenceCounter += 1 - self.pool = self.pool[1:] - #assert(len(self.pool) > 0) - - - - -if __name__ == '__main__': - import sys - import os.path - from metadata import MetaData - - if False: - if (len(sys.argv) != 2): - print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' - sys.exit(1) - else: - p0 = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] - p1 = [FILENAME, NAME_EXTENSION, LOWERCASE] - p2 = [METADATA, APERTURE, None] - p3 = [FILENAME, IMAGE_NUMBER, IMAGE_NUMBER_ALL] - p4 = [METADATA, CAMERA_MODEL, ORIGINAL_CASE] - p5 = [TEXT, '-', None] - p6 = [TEXT, 'Job', None] - - p = [p0, p1, p2, p3, p4] - p = [p6 + p5 + p2 + p5 + p3] - - d0 = [DATE_TIME, IMAGE_DATE, 'YYYYMMDD'] - d1 = [DATE_TIME, IMAGE_DATE, 'HHMMSS'] - d2 = [DATE_TIME, IMAGE_DATE, SUBSECONDS] - - d = [d0 + d1 + d2] - - fullpath = sys.argv[1] - path, filename = os.path.split(fullpath) - - m = MetaData(fullpath) - m.readMetadata() - - for pref in p: - i = ImageRenamePreferences(pref, None) - print i.generateNameUsingPreferences(m, filename) - - for pref in d: - i = ImageRenamePreferences(pref, None) - print i.generateNameUsingPreferences(m, filename) - else: - prefs = [SEQUENCES, SESSION_SEQ_NUMBER, SEQUENCE_NUMBER_3] -# prefs = ['Filename2', NAME_EXTENSION, UPPERCASE] - print checkPreferenceValid(DICT_IMAGE_RENAME_L0, prefs) diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py new file mode 100644 index 0000000..a0969f0 --- /dev/null +++ b/rapid/rpdfile.py @@ -0,0 +1,300 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import gtk + +import time, datetime + +import multiprocessing, logging +logger = multiprocessing.get_logger() + +import pyexiv2 + +import paths + +from gettext import gettext as _ + +import config +import metadataphoto +import metadatavideo + +import problemnotification as pn + +import thumbnail as tn + + +RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw', + 'nef', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', 'srw'] + +NON_RAW_IMAGE_EXTENSIONS = ['jpg', 'jpe', 'jpeg', 'tif', 'tiff'] + +PHOTO_EXTENSIONS = RAW_EXTENSIONS + NON_RAW_IMAGE_EXTENSIONS + +if metadatavideo.DOWNLOAD_VIDEO: + # some distros do not include the necessary libraries that Rapid Photo Downloader + # needs to be able to download videos + VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', + 'tod'] + VIDEO_THUMBNAIL_EXTENSIONS = ['thm'] +else: + VIDEO_EXTENSIONS = [] + VIDEO_THUMBNAIL_EXTENSIONS = [] + + +FILE_TYPE_PHOTO = 0 +FILE_TYPE_VIDEO = 1 + + +def file_type(file_extension): + """ + Uses file extentsion to determine the type of file - photo or video. + + Returns True if yes, else False. + """ + if file_extension in PHOTO_EXTENSIONS: + return FILE_TYPE_PHOTO + elif file_extension in VIDEO_EXTENSIONS: + return FILE_TYPE_VIDEO + return None + +def get_rpdfile(extension, name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id): + + if extension in VIDEO_EXTENSIONS: + return Video(name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id) + else: + # assume it's a photo - no check for performance reasons (this will be + # called many times) + return Photo(name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id) + +class FileTypeCounter: + def __init__(self): + self._counter = dict() + + def add(self, file_type): + self._counter[file_type] = self._counter.setdefault(file_type, 0) + 1 + + def file_types_present(self): + """ + returns a string to be displayed to the user that can be used + to show if a value refers to photos or videos or both, or just one + of each + """ + + no_videos = self._counter.setdefault(FILE_TYPE_VIDEO, 0) + no_images = self._counter.setdefault(FILE_TYPE_PHOTO, 0) + + if (no_videos > 0) and (no_images > 0): + v = _('photos and videos') + elif (no_videos == 0) and (no_images == 0): + v = _('photos or videos') + elif no_videos > 0: + if no_videos > 1: + v = _('videos') + else: + v = _('video') + else: + if no_images > 1: + v = _('photos') + else: + v = _('photo') + return v + + def count_files(self): + i = 0 + for key in self._counter: + i += self._counter[key] + return i + + def summarize_file_count(self): + #Number of files, e.g. "433 photos and videos" or "23 videos". + #Displayed in the progress bar at the top of the main application + #window. + file_types_present = self.file_types_present() + file_count_summary = _("%(number)s %(filetypes)s") % \ + {'number':self.count_files(), + 'filetypes': file_types_present} + return (file_count_summary, file_types_present) + +class RPDFile: + """ + Base class for photo or video file, with metadata + """ + + def __init__(self, name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id): + + self.path = path + + self.name = name + self.display_name = display_name + + self.full_file_name = os.path.join(path, name) + + self.size = size # type int + + self.modification_time = file_system_modification_time + + self.status = config.STATUS_NOT_DOWNLOADED + self.problem = None # class Problem in problemnotifcation.py + + self._assign_file_type() + + self.scan_pid = scan_pid + self.file_id = file_id + self.unique_id = str(scan_pid) + ":" + file_id + + self.problem = None + self.job_code = None + + # generated values + + self.temp_full_file_name = '' + self.download_start_time = None + + self.download_subfolder = '' + self.download_path = '' + self.download_name = '' + self.download_full_file_name = '' + + self.metadata = None + + # Values that will be inserted in download process -- + # (commented out because they're not needed until then) + + #self.sequences = None + #self.download_folder + #self.subfolder_pref_list = [] + #self.name_pref_list = [] + #strip_characters = False + + + def _assign_file_type(self): + self.file_type = None + + def initialize_problem(self): + self.problem = pn.Problem() + # these next values are used to display in the error log window + # the information in them can vary from other forms of display of errors + self.error_title = self.error_msg = self.error_extra_detail = '' + + def has_problem(self): + if self.problem is None: + return False + else: + return self.problem.has_problem() + + def add_problem(self, component, problem_definition, *args): + if self.problem is None: + self.initialize_problem() + self.problem.add_problem(component, problem_definition, *args) + + def add_extra_detail(self, extra_detail, *args): + self.problem.add_extra_detail(extra_detail, *args) + + + +#~ exif_tags_needed = ('Exif.Photo.FNumber', + #~ 'Exif.Photo.ISOSpeedRatings', + #~ 'Exif.Photo.ExposureTime', + #~ 'Exif.Photo.FocalLength', + #~ 'Exif.Image.Make', + #~ 'Exif.Image.Model', + #~ 'Exif.Canon.SerialNumber', + #~ 'Exif.Nikon3.SerialNumber' + #~ 'Exif.OlympusEq.SerialNumber', + #~ 'Exif.Olympus.SerialNumber', + #~ 'Exif.Olympus.SerialNumber2', + #~ 'Exif.Panasonic.SerialNumber', + #~ 'Exif.Fujifilm.SerialNumber', + #~ 'Exif.Image.CameraSerialNumber', + #~ 'Exif.Nikon3.ShutterCount', + #~ 'Exif.Canon.FileNumber', + #~ 'Exif.Canon.ImageNumber', + #~ 'Exif.Canon.OwnerName', + #~ 'Exif.Photo.DateTimeOriginal', + #~ 'Exif.Image.DateTime', + #~ 'Exif.Photo.SubSecTimeOriginal', + #~ 'Exif.Image.Orientation' + #~ ) + +class Photo(RPDFile): + + title = _("photo") + title_capitalized = _("Photo") + + def _assign_file_type(self): + self.file_type = FILE_TYPE_PHOTO + + def load_metadata(self): + + self.metadata = metadataphoto.MetaData(self.full_file_name) + try: + self.metadata.read() + except: + logger.warning("Could not read metadata from %s" % self.full_file_name) + return False + else: + return True + + +class Video(RPDFile): + + title = _("video") + title_capitalized = _("Video") + + def _assign_file_type(self): + self.file_type = FILE_TYPE_VIDEO + + def load_metadata(self): + self.metadata = metadatavideo.VideoMetaData(self.full_file_name) + return True + +class SamplePhoto(Photo): + def __init__(self, sample_name='IMG_0524.CR2', sequences=None): + Photo.__init__(self, name=sample_name, + display_name=sample_name, + path='/media/EOS_DIGITAL/DCIM/100EOS5D', + size=23516764, + file_system_modification_time=time.time(), + scan_pid=2033, + file_id='9873afe') + self.sequences = sequences + self.metadata = metadataphoto.DummyMetaData() + self.download_start_time = datetime.datetime.now() + +class SampleVideo(Video): + def __init__(self, sample_name='MVI_1379.MOV', sequences=None): + Video.__init__(self, name=sample_name, + display_name=sample_name, + path='/media/EOS_DIGITAL/DCIM/100EOS5D', + size=823513764, + file_system_modification_time=time.time(), + scan_pid=2033, + file_id='9873qrsfe') + self.sequences = sequences + self.metadata = metadatavideo.DummyMetaData(filename=sample_name) + self.download_start_time = datetime.datetime.now() diff --git a/rapid/rpdmultiprocessing.py b/rapid/rpdmultiprocessing.py new file mode 100644 index 0000000..4a06fc9 --- /dev/null +++ b/rapid/rpdmultiprocessing.py @@ -0,0 +1,26 @@ +# -*- coding: latin1 -*- +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +CONN_PARTIAL = 0 +CONN_COMPLETE = 1 + +MSG_BYTES = 0 +MSG_FILE = 1 +MSG_TEMP_DIRS = 2 +MSG_THUMB = 3 + +MSG_SEQUENCE_VALUE = 0 diff --git a/rapid/scan.py b/rapid/scan.py new file mode 100755 index 0000000..e55f43d --- /dev/null +++ b/rapid/scan.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import multiprocessing + +import gio +import gtk + +import pyexiv2 + +import rpdmultiprocessing as rpdmp +import rpdfile + + +import logging +logger = multiprocessing.get_logger() + +# python whitespace is significant - don't remove the leading whitespace on +# the second line + +file_attributes = "standard::name,standard::display-name,\ +standard::type,standard::size,time::modified,access::can-read,id::file" + + + +class Scan(multiprocessing.Process): + + """Scans the given path for files of a specified type. + + Returns results in batches, finishing with a total of the size of all the + files in bytes. + """ + + def __init__(self, path, batch_size, generate_folder, results_pipe, + terminate_queue, run_event): + + """Setup values needed to conduct the scan. + + 'path' is a string of the path to be scanned, which is passed to gio. + + 'batch_size' is the number of files that should be sent back to the + calling function at one time. + + 'results_pipe' is a connection on which to send the results. + + 'terminate_queue' is a queue whose sole purpose is to notify the + process that it should terminate and not return any results. + + 'run_event' is an Event that is used to temporarily halt execution. + + """ + + multiprocessing.Process.__init__(self) + self.path = path + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.run_event = run_event + self.batch_size = batch_size + self.generate_folder = generate_folder + self.counter = 0 + self.files = [] + self.file_type_counter = rpdfile.FileTypeCounter() + + + def _gio_scan(self, path, file_size_sum): + """recursive function to scan a directory and its subdirectories + for photos and possibly videos""" + + children = path.enumerate_children(file_attributes) + + for child in children: + + # pause if instructed by the caller + self.run_event.wait() + + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + logger.info("terminating scan...") + self.files = [] + return None + + # only collect files and scan in directories we can actually read + # cannot assume that users will download only from memory cards + + if child.get_attribute_boolean(gio.FILE_ATTRIBUTE_ACCESS_CAN_READ): + file_type = child.get_file_type() + name = child.get_name() + if file_type == gio.FILE_TYPE_DIRECTORY: + file_size_sum = self._gio_scan(path.get_child(name), + file_size_sum) + if file_size_sum is None: + return None + + elif file_type == gio.FILE_TYPE_REGULAR: + ext = os.path.splitext(name)[1].lower()[1:] + + file_type = rpdfile.file_type(ext) + if file_type is not None: + # count how many files of each type are included + # e.g. photo, video + self.file_type_counter.add(file_type) + self.counter += 1 + display_name = child.get_display_name() + size = child.get_size() + modification_time = child.get_modification_time() + file_id = child.get_attribute_string( + gio.FILE_ATTRIBUTE_ID_FILE) + scanned_file = rpdfile.get_rpdfile(ext, + name, + display_name, + path.get_path(), + size, + modification_time, + self.pid, + file_id) + + if self.generate_folder: + # this dramatically slows scanning speed, and it + # is unlikely this will be called this early in the + # workflow + scanned_file.read_metadata() + + self.files.append(scanned_file) + + if self.counter == self.batch_size: + # send batch of results + self.results_pipe.send((rpdmp.CONN_PARTIAL, + self.files)) + self.files = [] + self.counter = 0 + + file_size_sum += size + + return file_size_sum + + + def run(self): + """start the actual scan.""" + source = gio.File(self.path) + try: + size = self._gio_scan(source, 0) + except gio.Error, inst: + logger.error("Error while scanning %s: %s", self.path, inst) + size = None + + if size is not None: + if self.counter > 0: + # send any remaining results + self.results_pipe.send((rpdmp.CONN_PARTIAL, self.files)) + self.results_pipe.send((rpdmp.CONN_COMPLETE, (size, + self.file_type_counter, self.pid))) + self.results_pipe.close() diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py new file mode 100644 index 0000000..7e4a495 --- /dev/null +++ b/rapid/subfolderfile.py @@ -0,0 +1,474 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Generates names for files and folders. + +Runs a daemon process. +""" + +import os, datetime, collections + +import gio +import multiprocessing +import logging +logger = multiprocessing.get_logger() + + +import rpdfile +import rpdmultiprocessing as rpdmp +import generatename as gn +import problemnotification as pn +import prefsrapid +import config + +from gettext import gettext as _ + +class SyncRawJpeg: + def __init__(self): + self.photos = {} + + def add_download(self, name, extension, date_time, sub_seconds, sequence_number_used): + if name not in self.photos: + self.photos[name] = ([extension], date_time, sub_seconds, sequence_number_used) + else: + if extension not in self.photos[name][0]: + self.photos[name][0].append(extension) + + + def matching_pair(self, name, extension, date_time, sub_seconds): + """Checks to see if the image matches an image that has already been downloaded. + Image name (minus extension), exif date time, and exif subseconds are checked. + + Returns -1 and a sequence number if the name, extension, and exif values match (i.e. it has already been downloaded) + Returns 0 and a sequence number if name and exif values match, but the extension is different (i.e. a matching RAW + JPG image) + Returns -99 and a sequence number of None if photos detected with the same filenames, but taken at different times + Returns 1 and a sequence number of None if no match""" + + if name in self.photos: + if self.photos[name][1] == date_time and self.photos[name][2] == sub_seconds: + if extension in self.photos[name][0]: + return (-1, self.photos[name][3]) + else: + return (0, self.photos[name][3]) + else: + return (-99, None) + return (1, None) + + def ext_exif_date_time(self, name): + """Returns first extension, exif date time and subseconds data for the already downloaded photo""" + return (self.photos[name][0][0], self.photos[name][1], self.photos[name][2]) + +def time_subseconds_human_readable(date, subseconds): + return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ + {'hour':date.strftime("%H"), + 'minute':date.strftime("%M"), + 'second':date.strftime("%S"), + 'subsecond': subseconds} + +def load_metadata(rpd_file): + """ + Loads the metadata for the file. Returns True if operation succeeded, false + otherwise + """ + if rpd_file.metadata is None: + if not rpd_file.load_metadata(): + # Error in reading metadata + rpd_file.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': rpd_file.title_capitalized}) + return False + return True + + +def _generate_name(generator, rpd_file): + + do_generation = True + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + do_generation = load_metadata(rpd_file) + else: + if rpd_file.metadata is None: + rpd_file.load_metadata() + + if do_generation: + value = generator.generate_name(rpd_file) + if value is None: + value = '' + else: + value = '' + + return value + +def generate_subfolder(rpd_file): + + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + generator = gn.PhotoSubfolder(rpd_file.subfolder_pref_list) + else: + generator = gn.VideoSubfolder(rpd_file.subfolder_pref_list) + + rpd_file.download_subfolder = _generate_name(generator, rpd_file) + return rpd_file + +def generate_name(rpd_file): + do_generation = True + + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + generator = gn.PhotoName(rpd_file.name_pref_list) + else: + generator = gn.VideoName(rpd_file.name_pref_list) + + rpd_file.download_name = _generate_name(generator, rpd_file) + return rpd_file + + +class SubfolderFile(multiprocessing.Process): + def __init__(self, results_pipe, sequence_values): + multiprocessing.Process.__init__(self) + self.daemon = True + self.results_pipe = results_pipe + + self.downloads_today = sequence_values[0] + self.downloads_today_date = sequence_values[1] + self.day_start = sequence_values[2] + self.refresh_downloads_today = sequence_values[3] + self.stored_sequence_no = sequence_values[4] + self.uses_stored_sequence_no = sequence_values[5] + self.uses_session_sequece_no = sequence_values[6] + self.uses_sequence_letter = sequence_values[7] + + logger.debug("Start of day is set to %s", self.day_start.value) + + def progress_callback_no_update(self, amount_downloaded, total): + pass + + def file_exists(self, rpd_file, identifier=None): + """ + Notify user that the download file already exists + """ + # get information on when the existing file was last modified + try: + modification_time = os.path.getmtime(rpd_file.download_full_file_name) + dt = datetime.datetime.fromtimestamp(modification_time) + date = dt.strftime("%x") + time = dt.strftime("%X") + except: + logger.warning("Could not determine the file modification time of %s", + rpd_file.download_full_file_name) + date = time = '' + + if not identifier: + rpd_file.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD, + {'filetype':rpd_file.title_capitalized}) + rpd_file.add_extra_detail(pn.EXISTING_FILE, + {'filetype': rpd_file.title, + 'date': date, 'time': time}) + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + rpd_file.error_extra_detail = pn.extra_detail_definitions[pn.EXISTING_FILE] % \ + {'date':date, 'time':time, 'filetype': rpd_file.title} + else: + rpd_file.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED, + {'filetype':rpd_file.title_capitalized}) + rpd_file.add_extra_detail(pn.UNIQUE_IDENTIFIER, + {'identifier': identifier, + 'filetype': rpd_file.title, + 'date': date, 'time': time}) + rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING + rpd_file.error_extra_detail = pn.extra_detail_definitions[pn.UNIQUE_IDENTIFIER] % \ + {'identifier': identifier, 'filetype': rpd_file.title, + 'date': date, 'time': time} + rpd_file.error_title = rpd_file.problem.get_title() + rpd_file.error_msg = _("Source: %(source)s\nDestination: %(destination)s") \ + % {'source': rpd_file.full_file_name, + 'destination': rpd_file.download_full_file_name} + return rpd_file + + def download_failure_file_error(self, rpd_file, inst): + """ + Handle cases where file failed to download + """ + rpd_file.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': rpd_file.title}) + rpd_file.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_DETAIL, inst) + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + logger.error("Failed to create file %s: %s", rpd_file.download_full_file_name, inst) + + rpd_file.error_title = rpd_file.problem.get_title() + rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ + {'problem': rpd_file.problem.get_problems(), + 'file': rpd_file.full_file_name} + + return rpd_file + + def same_name_different_exif(self, sync_photo_name, rpd_file): + """Notify the user that a file was already downloaded with the same name, but the exif information was different""" + i1_ext, i1_date_time, i1_subseconds = self.sync_raw_jpeg.ext_exif_date_time(sync_photo_name) + detail = {'image1': "%s%s" % (sync_photo_name, i1_ext), + 'image1_date': i1_date_time.strftime("%x"), + 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds), + 'image2': rpd_file.name, + 'image2_date': rpd_file.metadata.date_time().strftime("%x"), + 'image2_time': time_subseconds_human_readable( + rpd_file.metadata.date_time(), + rpd_file.metadata.sub_seconds())} + rpd_file.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail) + + rpd_file.error_title = _('Photos detected with the same filenames, but taken at different times') + rpd_file.error_msg = pn.problem_definitions[pn.SAME_FILE_DIFFERENT_EXIF][1] % detail + rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING + return rpd_file + + + def run(self): + """ + Get subfolder and name. + Attempt to move the file from it's temporary directory. + If successful, increment sequence values. + Report any success or failure. + """ + i = 0 + download_count = 0 + + duplicate_files = {} + + + # Track downloads today, using a class whose purpose is to + # take the value in the user prefs, increment, and then be used + # to update the prefs (which can only happen via the main process) + self.downloads_today_tracker = prefsrapid.DownloadsTodayTracker( + day_start = self.day_start.value, + downloads_today = self.downloads_today.value, + downloads_today_date = self.downloads_today_date.value) + + # Track sequences using shared downloads today and stored sequence number + # (shared with main process) + self.sequences = gn.Sequences(self.downloads_today_tracker, + self.stored_sequence_no.value) + + self.sync_raw_jpeg = SyncRawJpeg() + + + while True: + logger.debug("Finished %s. Getting next task.", download_count) + + task = self.results_pipe.recv() + + # rename file and move to generated subfolder + download_succeeded, download_count, rpd_file = task + + move_succeeded = False + + + if download_succeeded: + temp_file = gio.File(rpd_file.temp_full_file_name) + + synchronize_raw_jpg_failed = False + if not (rpd_file.synchronize_raw_jpg and + rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO): + synchronize_raw_jpg = False + sequence_to_use = None + else: + synchronize_raw_jpg = True + sync_photo_name, sync_photo_ext = os.path.splitext(rpd_file.name) + if not load_metadata(rpd_file): + synchronize_raw_jpg_failed = True + else: + j, sequence_to_use = self.sync_raw_jpeg.matching_pair( + name=sync_photo_name, extension=sync_photo_ext, + date_time=rpd_file.metadata.date_time(), + sub_seconds=rpd_file.metadata.sub_seconds()) + if j == -1: + # this exact file has already been downloaded (same extension, same filename, and same exif date time subsecond info) + if (rpd_file.download_conflict_resolution <> + config.ADD_UNIQUE_IDENTIFIER): + rpd_file.add_problem(None, pn.FILE_ALREADY_DOWNLOADED, {'filetype': rpd_file.title_capitalized}) + rpd_file.error_title = _('Photo has already been downloaded') + rpd_file.error_msg = _("Source: %(source)s") % {'source': rpd_file.full_file_name} + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + synchronize_raw_jpg_failed = True + else: + self.sequences.set_matched_sequence_value(sequence_to_use) + if j == -99: + rpd_file = self.same_name_different_exif(sync_photo_name, rpd_file) + + if synchronize_raw_jpg_failed: + generation_succeeded = False + else: + # Generate subfolder name and new file name + generation_succeeded = True + rpd_file = generate_subfolder(rpd_file) + if rpd_file.download_subfolder: + + if self.refresh_downloads_today.value: + # overwrite downloads today value tracked here, + # as user has modified their preferences + self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today.value) + self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date.value) + self.downloads_today_tracker.day_start = self.day_start.value + self.refresh_downloads_today.value = False + + # update whatever the stored value is + self.sequences.stored_sequence_no = self.stored_sequence_no.value + rpd_file.sequences = self.sequences + + # generate the file name + rpd_file = generate_name(rpd_file) + + if rpd_file.has_problem(): + rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING + rpd_file.error_title = rpd_file.problem.get_title() + rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ + {'problem': rpd_file.problem.get_problems(), + 'file': rpd_file.full_file_name} + + # Check for any errors + if not rpd_file.download_subfolder or not rpd_file.download_name: + if not rpd_file.download_subfolder and not rpd_file.download_name: + area = _("subfolder and filename") + elif not rpd_file.download_name: + area = _("filename") + else: + area = _("subfolder") + rpd_file.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': rpd_file.title_capitalized, 'area': area}) + rpd_file.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) + generation_succeeded = False + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + + rpd_file.error_title = rpd_file.problem.get_title() + rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ + {'problem': rpd_file.problem.get_problems(), + 'file': rpd_file.full_file_name} + + + if generation_succeeded: + rpd_file.download_path = os.path.join(rpd_file.download_folder, rpd_file.download_subfolder) + rpd_file.download_full_file_name = os.path.join(rpd_file.download_path, rpd_file.download_name) + + subfolder = gio.File(path=rpd_file.download_path) + + # Create subfolder if it does not exist. + # It is possible to skip the query step, and just try to create + # the directories and ignore the error of it already existing - + # but it takes twice as long to fail with an error than just + # run the straight query + + if not subfolder.query_exists(cancellable=None): + try: + subfolder.make_directory_with_parents(cancellable=gio.Cancellable()) + except gio.Error, inst: + # The directory may have been created by another process + # between the time it takes to query and the time it takes + # to create a new directory. Ignore such errors. + if inst.code <> gio.ERROR_EXISTS: + logger.error("Failed to create download subfolder: %s", rpd_file.download_path) + logger.error(inst) + rpd_file.error_title = _("Failed to create download subfolder") + rpd_file.error_msg = _("Path: %s") % rpd_file.download_path + + # Move temp file to subfolder + + download_file = gio.File(rpd_file.download_full_file_name) + + add_unique_identifier = False + try: + temp_file.move(download_file, self.progress_callback_no_update, cancellable=None) + move_succeeded = True + if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING: + rpd_file.status = config.STATUS_DOWNLOADED + except gio.Error, inst: + if inst.code == gio.ERROR_EXISTS: + if (rpd_file.download_conflict_resolution == + config.ADD_UNIQUE_IDENTIFIER): + add_unique_identifier = True + else: + rpd_file = self.file_exists(rpd_file) + else: + rpd_file = self.download_failure_file_error(rpd_file, inst) + + if add_unique_identifier: + name = os.path.splitext(rpd_file.download_name) + full_name = rpd_file.download_full_file_name + suffix_already_used = True + while suffix_already_used: + duplicate_files[full_name] = duplicate_files.get( + full_name, 0) + 1 + identifier = '_%s' % duplicate_files[full_name] + rpd_file.download_name = name[0] + identifier + name[1] + rpd_file.download_full_file_name = os.path.join( + rpd_file.download_path, + rpd_file.download_name) + download_file = gio.File( + rpd_file.download_full_file_name) + + try: + temp_file.move(download_file, self.progress_callback_no_update, cancellable=None) + move_succeeded = True + suffix_already_used = False + rpd_file = self.file_exists(rpd_file, identifier) + logger.error("%s: %s - %s", rpd_file.full_file_name, + rpd_file.problem.get_title(), + rpd_file.problem.get_problems()) + except gio.Error, inst: + if inst.code <> gio.ERROR_EXISTS: + rpd_file = self.download_failure_file_error(rpd_file, inst) + + + + logger.debug("Finish processing file: %s", download_count) + + if move_succeeded: + if synchronize_raw_jpg: + if sequence_to_use is None: + sequence = self.sequences.create_matched_sequences() + else: + sequence = sequence_to_use + self.sync_raw_jpeg.add_download(name=sync_photo_name, + extension=sync_photo_ext, + date_time=rpd_file.metadata.date_time(), + sub_seconds=rpd_file.metadata.sub_seconds(), + sequence_number_used=sequence) + if sequence_to_use is None: + if self.uses_session_sequece_no.value or self.uses_sequence_letter.value: + self.sequences.increment( + self.uses_session_sequece_no.value, + self.uses_sequence_letter.value) + if self.uses_stored_sequence_no.value: + self.stored_sequence_no.value += 1 + self.downloads_today_tracker.increment_downloads_today() + self.downloads_today.value = self.downloads_today_tracker.get_raw_downloads_today() + self.downloads_today_date.value = self.downloads_today_tracker.get_raw_downloads_today_date() + + if not move_succeeded: + logger.error("%s: %s - %s", rpd_file.full_file_name, + rpd_file.problem.get_title(), + rpd_file.problem.get_problems()) + try: + temp_file.delete(cancellable=None) + except gio.Error, inst: + logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name) + logger.error(inst) + + + + + + rpd_file.metadata = None #purge metadata, as it cannot be pickled + rpd_file.sequences = None + self.results_pipe.send((move_succeeded, rpd_file,)) + + i += 1 + + + diff --git a/rapid/tableplusminus.py b/rapid/tableplusminus.py index ab7e7c1..5713474 100644 --- a/rapid/tableplusminus.py +++ b/rapid/tableplusminus.py @@ -44,15 +44,15 @@ class TablePlusMinus(gtk.Table): def __init__(self, rows=1, columns=1, homogeneous=False): if not self.debug: gtk.Table.__init__(self, rows, columns + 2, homogeneous) - self.extraCols = 2 # representing minus and plus buttons + self.extra_cols = 2 # representing minus and plus buttons else: gtk.Table.__init__(self, rows, columns + 3, homogeneous) - self.extraCols = 3 # representing minus and plus buttons, and info label + self.extra_cols = 3 # representing minus and plus buttons, and info label # no of columns NOT including the + and - buttons - self.pm_noColumns = columns + self.pm_no_columns = columns # how many rows there are in the gtk.Table - self.pm_noRows = rows + self.pm_no_rows = rows # list of widgets in the gtk.Table self.pm_rows = [] # dict of callback ids for minus and plus buttons @@ -67,39 +67,39 @@ class TablePlusMinus(gtk.Table): self.set_col_spacing(columns+1, hd.CONTROL_IN_TABLE_SPACE) self.set_row_spacings(hd.CONTROL_IN_TABLE_SPACE) - def _setMinusButtonSensitivity(self): - button = self.pm_rows[0][self.pm_noColumns] + def _set_minus_button_sensitivity(self): + button = self.pm_rows[0][self.pm_no_columns] if len(self.pm_rows) == 1: button.set_sensitive(False) else: button.set_sensitive(True) - def _createMinusPlusButtons(self, rowPosition): + def _create_minus_plus_buttons(self, row_position): plus_button = gtk.Button() plus_button.set_image(gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU)) - self._createCallback(plus_button, rowPosition, 'clicked', self.on_plus_button_clicked) + self._create_callback(plus_button, row_position, 'clicked', self.on_plus_button_clicked) minus_button = gtk.Button() minus_button.set_image(gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)) - self._createCallback(minus_button, rowPosition, 'clicked', self.on_minus_button_clicked) + self._create_callback(minus_button, row_position, 'clicked', self.on_minus_button_clicked) return minus_button, plus_button def append(self, row): - self.insertAfter(len(self.pm_rows)-1, row) + self.insert_after(len(self.pm_rows)-1, row) - def _getMinusAndPlusButtonsForRow(self, rowPosition): + def _get_minus_and_plus_buttons_for_row(self, row_position): """ - Return as a tuple minus and plus buttons for the row specified by rowPosition + Return as a tuple minus and plus buttons for the row specified by row_position """ - return (self.pm_rows[rowPosition][self.pm_noColumns], self.pm_rows[rowPosition][self.pm_noColumns+1]) + return (self.pm_rows[row_position][self.pm_no_columns], self.pm_rows[row_position][self.pm_no_columns+1]) - def removeRow(self, rowPosition): + def remove_row(self, row_position): # remove widgets from table - for col in range(self.pm_noColumns + self.extraCols): - widget = self.pm_rows[rowPosition][col] + for col in range(self.pm_no_columns + self.extra_cols): + widget = self.pm_rows[row_position][col] if widget: self.remove(widget) if self.pm_callbacks.has_key(widget): @@ -108,61 +108,61 @@ class TablePlusMinus(gtk.Table): # reposition existing rows in gtk.Table - self._moveRows(-1, rowPosition + 1) + self._move_rows(-1, row_position + 1) # remove row from list of rows - del self.pm_rows[rowPosition] + del self.pm_rows[row_position] - self._setMinusButtonSensitivity() - self.pm_noRows -= 1 - self.resize(self.pm_noRows, self.pm_noColumns + self.extraCols) - self._printDebugInfo() + self._set_minus_button_sensitivity() + self.pm_no_rows -= 1 + self.resize(self.pm_no_rows, self.pm_no_columns + self.extra_cols) + self._print_debug_info() - def _createCallback(self, widget, rowPosition, callbackType = None, callbackMethod=None): - if callbackType: - self.pm_callbacks[widget] = widget.connect(callbackType, callbackMethod, rowPosition) + def _create_callback(self, widget, row_position, callback_type = None, callbackMethod=None): + if callback_type: + self.pm_callbacks[widget] = widget.connect(callback_type, callbackMethod, row_position) else: name = widget.get_name() if name == 'GtkComboBox': - self.pm_callbacks[widget] = widget.connect("changed", self.on_combobox_changed, rowPosition) + self.pm_callbacks[widget] = widget.connect("changed", self.on_combobox_changed, row_position) elif name == 'GtkEntry': - self.pm_callbacks[widget] = widget.connect("changed", self.on_entry_changed, rowPosition) + self.pm_callbacks[widget] = widget.connect("changed", self.on_entry_changed, row_position) - def _moveRows(self, adjustment, startRow, endRow = -1): + def _move_rows(self, adjustment, start_row, end_row = -1): """ Moves gtk.Table rows up or down according to adjustment (which MUST be -1 or 1). - Starts at row startRow and ends at row endRow. If endRow == -1, then goes to last row in table. + Starts at row start_row and ends at row end_row. If end_row == -1, then goes to last row in table. Readjusts callbacks. """ - if endRow == -1: - endRow = len(self.pm_rows) - for r in range(startRow, endRow): + if end_row == -1: + end_row = len(self.pm_rows) + for r in range(start_row, end_row): if self.debug: - print "Row %s becomes row %s" % (self.pm_rows[r][self.pm_noColumns + 2].get_label(), r + adjustment) - self.pm_rows[r][self.pm_noColumns + 2].set_label(str(r + adjustment)) + print "Row %s becomes row %s" % (self.pm_rows[r][self.pm_no_columns + 2].get_label(), r + adjustment) + self.pm_rows[r][self.pm_no_columns + 2].set_label(str(r + adjustment)) - for col in range(self.pm_noColumns + self.extraCols): + for col in range(self.pm_no_columns + self.extra_cols): widget = self.pm_rows[r][col] if widget: self.remove(widget) widget.disconnect(self.pm_callbacks[widget]) self.attach(widget, col, col+1, r + adjustment, r + adjustment + 1) - if col == self.pm_noColumns: - self._createCallback(widget, r + adjustment, 'clicked', self.on_minus_button_clicked) - elif col == self.pm_noColumns + 1: - self._createCallback(widget, r + adjustment, 'clicked', self.on_plus_button_clicked) + if col == self.pm_no_columns: + self._create_callback(widget, r + adjustment, 'clicked', self.on_minus_button_clicked) + elif col == self.pm_no_columns + 1: + self._create_callback(widget, r + adjustment, 'clicked', self.on_plus_button_clicked) else: - self._createCallback(widget, r + adjustment) + self._create_callback(widget, r + adjustment) - def _printDebugInfo(self): + def _print_debug_info(self): if self.debug: print "\nRows in internal list: %s\nTable rows: %s" % \ - (len(self.pm_rows), self.pm_noRows) + (len(self.pm_rows), self.pm_no_rows) - if len(self.pm_rows) <> self.pm_noRows: + if len(self.pm_rows) <> self.pm_no_rows: print "|\n\\\n --> Unequal no. of rows" @@ -172,42 +172,42 @@ class TablePlusMinus(gtk.Table): """ Override base class attach method, to allow automatic shrinking of minus and plus buttons """ - if left_attach >= self.pm_noColumns and left_attach <= self.pm_noColumns + 1: + if left_attach >= self.pm_no_columns and left_attach <= self.pm_no_columns + 1: # since we are adding plus or minus button, shrink the button gtk.Table.attach(self, child, left_attach, right_attach, top_attach, bottom_attach, gtk.SHRINK, gtk.SHRINK, xpadding, ypadding) else: gtk.Table.attach(self, child, left_attach, right_attach, top_attach, bottom_attach, xoptions, yoptions, xpadding, ypadding) - def insertAfter(self, rowPosition, row): + def insert_after(self, row_position, row): """ - Inserts row into the table at row following rowPosition + Inserts row into the table at row following row_position """ #is table big enough? - self.checkTableRowsAndAdjust() + self.check_table_rows_and_adjust() #move (reattach) other widgets & readjust connect - self._moveRows(1, rowPosition + 1) + self._move_rows(1, row_position + 1) # insert row - for col in range(self.pm_noColumns): + for col in range(self.pm_no_columns): widget = row[col] if widget: - self._createCallback(widget, rowPosition+1) - self.attach(widget, col, col+1, rowPosition+1, rowPosition+2) + self._create_callback(widget, row_position+1) + self.attach(widget, col, col+1, row_position+1, row_position+2) - minus_button, plus_button = self._createMinusPlusButtons(rowPosition+1) + minus_button, plus_button = self._create_minus_plus_buttons(row_position+1) row.append(minus_button) row.append(plus_button) - self.attach(minus_button, self.pm_noColumns, self.pm_noColumns+1, rowPosition+1, rowPosition+2) - self.attach(plus_button, self.pm_noColumns+1, self.pm_noColumns+2, rowPosition+1, rowPosition+2) + self.attach(minus_button, self.pm_no_columns, self.pm_no_columns+1, row_position+1, row_position+2) + self.attach(plus_button, self.pm_no_columns+1, self.pm_no_columns+2, row_position+1, row_position+2) if self.debug: - label = gtk.Label(str(rowPosition+1)) - self.attach(label, self.pm_noColumns+2, self.pm_noColumns+3, rowPosition+1, rowPosition+2) + label = gtk.Label(str(row_position+1)) + self.attach(label, self.pm_no_columns+2, self.pm_no_columns+3, row_position+1, row_position+2) row.append(label) @@ -217,66 +217,66 @@ class TablePlusMinus(gtk.Table): #adjust internal reference table - self.pm_rows.insert(rowPosition + 1, row) + self.pm_rows.insert(row_position + 1, row) - self._setMinusButtonSensitivity() + self._set_minus_button_sensitivity() - self._printDebugInfo() + self._print_debug_info() - def checkTableRowsAndAdjust(self, noRowsToAdd=1, adjustRows=True): - noRowsOk = True - if len(self.pm_rows) + noRowsToAdd > self.pm_noRows: - if adjustRows: - extraRowsToAdd = len(self.pm_rows) + noRowsToAdd - self.pm_noRows - self.pm_noRows += extraRowsToAdd - self.resize(self.pm_noRows, self.pm_noColumns + self.extraCols) + def check_table_rows_and_adjust(self, no_rows_to_add=1, adjust_rows=True): + no_rows_ok = True + if len(self.pm_rows) + no_rows_to_add > self.pm_no_rows: + if adjust_rows: + extra_rows_to_add = len(self.pm_rows) + no_rows_to_add - self.pm_no_rows + self.pm_no_rows += extra_rows_to_add + self.resize(self.pm_no_rows, self.pm_no_columns + self.extra_cols) else: - noRowsOk = False - return noRowsOk + no_rows_ok = False + return no_rows_ok - def getDefaultRow(self): + def get_default_row(self): """ Returns a list of default widgets to insert as a row into the table. Expected to be implemented in derived class. """ - return [None] * self.pm_noColumns + return [None] * self.pm_no_columns - def on_combobox_changed(self, widget, rowPosition): + def on_combobox_changed(self, widget, row_position): """ Callback for combobox that is expected to be implemented in derived class """ pass - def on_entry_changed(self, widget, rowPosition): + def on_entry_changed(self, widget, row_position): """ Callback for entry that is expected to be implemented in derived class """ pass - def _debugButtonPressed(self, buttonText, rowPosition): + def _debug_button_pressed(self, buttonText, row_position): if self.debug: t = datetime.datetime.now().strftime("%H:%M:%S") - print "\n****\n%s\n\n%s clicked at %s" %(t, buttonText, rowPosition) + print "\n****\n%s\n\n%s clicked at %s" %(t, buttonText, row_position) - def on_minus_button_clicked(self, widget, rowPosition): - self._debugButtonPressed("Minus", rowPosition) - self.removeRow(rowPosition) - self.on_rowDeleted(rowPosition) + def on_minus_button_clicked(self, widget, row_position): + self._debug_button_pressed("Minus", row_position) + self.remove_row(row_position) + self.on_row_deleted(row_position) - def on_plus_button_clicked(self, widget, rowPosition): - self._debugButtonPressed("Plus", rowPosition) - self.insertAfter(rowPosition, self.getDefaultRow()) - self.on_rowAdded(rowPosition) + def on_plus_button_clicked(self, widget, row_position): + self._debug_button_pressed("Plus", row_position) + self.insert_after(row_position, self.get_default_row()) + self.on_row_added(row_position) - def on_rowAdded(self, rowPosition): + def on_row_added(self, row_position): """ Expected to be implemented in derived class """ pass - def on_rowDeleted(self, rowPosition): + def on_row_deleted(self, row_position): """ Expected to be implemented in derived class """ diff --git a/rapid/thumbnail.py b/rapid/thumbnail.py new file mode 100644 index 0000000..e9c7e66 --- /dev/null +++ b/rapid/thumbnail.py @@ -0,0 +1,390 @@ +#!/usr/bin/python +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import multiprocessing +import types +import os + +import gtk + +import paths + +from PIL import Image +import cStringIO +import tempfile +import subprocess + +import rpdfile + +import rpdmultiprocessing as rpdmp +from utilities import image_to_pixbuf, pixbuf_to_image +import pyexiv2 + +from filmstrip import add_filmstrip + +import logging +logger = multiprocessing.get_logger() + + +def get_stock_photo_image(): + length = min(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/photo.svg'), length, length) + image = pixbuf_to_image(pixbuf) + return image + +def get_stock_photo_image_icon(): + image = Image.open(paths.share_dir('glade3/photo66.png')) + image = image.convert("RGBA") + return image + +def get_stock_video_image(): + length = min(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/video.svg'), length, length) + image = pixbuf_to_image(pixbuf) + return image + +def get_stock_video_image_icon(): + image = Image.open(paths.share_dir('glade3/video66.png')) + image = image.convert("RGBA") + return image + + +class PhotoIcons(): + stock_thumbnail_image_icon = get_stock_photo_image_icon() + +class VideoIcons(): + stock_thumbnail_image_icon = get_stock_video_image_icon() + +def upsize_pil(image, size): + width_max = size[0] + height_max = size[1] + width_orig = float(image.size[0]) + height_orig = float(image.size[1]) + if (width_orig / width_max) > (height_orig / height_max): + height = int((height_orig / width_orig) * width_max) + width = width_max + else: + width = int((width_orig / height_orig) * height_max) + height=height_max + + return image.resize((width, height), Image.ANTIALIAS) + +def downsize_pil(image, box, fit=False): + """Downsample the PIL image. + image: Image - an Image-object + box: tuple(x, y) - the bounding box of the result image + fix: boolean - crop the image to fill the box + + Code adpated from example by Christian Harms + Source: http://united-coders.com/christian-harms/image-resizing-tips-every-coder-should-know + """ + #preresize image with factor 2, 4, 8 and fast algorithm + factor = 1 + logger.debug("Image size %sx%s", image.size[0], image.size[1]) + logger.debug("Box size %sx%s", box[0],box[1]) + while image.size[0]/factor > 2*box[0] and image.size[1]*2/factor > 2*box[1]: + factor *=2 + if factor > 1: + logger.debug("quick resize %sx%s", image.size[0]/factor, image.size[1]/factor) + image.thumbnail((image.size[0]/factor, image.size[1]/factor), Image.NEAREST) + logger.debug("did first thumbnail") + + #calculate the cropping box and get the cropped part + if fit: + x1 = y1 = 0 + x2, y2 = image.size + wRatio = 1.0 * x2/box[0] + hRatio = 1.0 * y2/box[1] + if hRatio > wRatio: + y1 = y2/2-box[1]*wRatio/2 + y2 = y2/2+box[1]*wRatio/2 + else: + x1 = x2/2-box[0]*hRatio/2 + x2 = x2/2+box[0]*hRatio/2 + image = image.crop((x1,y1,x2,y2)) + + #Resize the image with best quality algorithm ANTI-ALIAS + logger.debug("about to actually downsize using image.thumbnail") + image.thumbnail(box, Image.ANTIALIAS) + logger.debug("it downsized") + +class PicklablePIL: + def __init__(self, image): + self.size = image.size + self.mode = image.mode + self.image_data = image.tostring() + + def get_image(self): + return Image.fromstring(self.mode, self.size, self.image_data) + + def get_pixbuf(self): + return image_to_pixbuf(self.get_image()) + +def get_video_THM_file(fullFileName): + """ + Checks to see if a thumbnail file (THM) is in the same directory as the + file. Expects a full path to be part of the file name. + + Returns the filename, including path, if found, else returns None. + """ + + f = None + name, ext = os.path.splitext(fullFileName) + for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS: + if os.path.exists(name + '.' + e): + f = name + '.' + e + break + if os.path.exists(name + '.' + e.upper()): + f = name + '.' + e.upper() + break + + return f + +class Thumbnail: + + # file types from which to remove letterboxing (black bands in the thumbnail + # previews) + crop_thumbnails = ('CR2', 'DNG', 'RAF', 'ORF', 'PEF', 'ARW') + + def _ignore_embedded_160x120_thumbnail(self, max_size_needed, metadata): + return max_size_needed is None or max_size_needed[0] > 160 or max_size_needed[1] > 120 or not metadata.exif_thumbnail.data + + def _get_thumbnail_data(self, metadata, max_size_needed): + logger.debug("Getting thumbnail data %s", max_size_needed) + if self._ignore_embedded_160x120_thumbnail(max_size_needed, metadata): + logger.debug("Ignoring embedded preview") + lowrez = False + previews = metadata.previews + if not previews: + return (None, None) + else: + if max_size_needed: + for thumbnail in previews: + if thumbnail.dimensions[0] >= max_size_needed or thumbnail.dimensions[1] >= max_size_needed: + break + else: + thumbnail = previews[-1] + else: + thumbnail = metadata.exif_thumbnail + lowrez = True + return (thumbnail.data, lowrez) + + def _process_thumbnail(self, image, size_reduced): + if image.mode <> "RGBA": + image = image.convert("RGBA") + + thumbnail = PicklablePIL(image) + if size_reduced is not None: + thumbnail_icon = image.copy() + downsize_pil(thumbnail_icon, size_reduced, fit=False) + thumbnail_icon = PicklablePIL(thumbnail_icon) + else: + thumbnail_icon = None + + return (thumbnail, thumbnail_icon) + + def _get_photo_thumbnail(self, full_file_name, size_max, size_reduced): + thumbnail = None + thumbnail_icon = None + name = os.path.basename(full_file_name) + metadata = pyexiv2.metadata.ImageMetadata(full_file_name) + try: + logger.debug("Read photo metadata...") + metadata.read() + except: + logger.warning("Could not read metadata from %s", full_file_name) + else: + logger.debug("...successfully read photo metadata") + if metadata.mime_type == "image/jpeg" and self._ignore_embedded_160x120_thumbnail(size_max, metadata): + try: + image = Image.open(full_file_name) + lowrez = False + except: + logger.warning("Could not generate thumbnail for jpeg %s ", full_file_name) + image = None + else: + thumbnail_data, lowrez = self._get_thumbnail_data(metadata, max_size_needed=size_max) + logger.debug("_get_thumbnail_data returned") + if not isinstance(thumbnail_data, types.StringType): + image = None + else: + td = cStringIO.StringIO(thumbnail_data) + logger.debug("got td") + try: + image = Image.open(td) + except: + logger.warning("Unreadable thumbnail for %s", full_file_name) + image = None + logger.debug("opened image") + if image: + try: + orientation = metadata['Exif.Image.Orientation'].value + except: + orientation = None + if lowrez: + # need to remove letterboxing / pillarboxing from some + # RAW thumbnails + if os.path.splitext(full_file_name)[1][1:].upper() in Thumbnail.crop_thumbnails: + image2 = image.crop((0, 8, 160, 112)) + image2.load() + image = image2 + if size_max is not None and (image.size[0] > size_max[0] or image.size[1] > size_max[1]): + logger.debug("downsizing") + downsize_pil(image, size_max, fit=False) + logger.debug("downsized") + if orientation == 8: + # rotate counter clockwise + image = image.rotate(90) + elif orientation == 6: + # rotate clockwise + image = image.rotate(270) + elif orientation == 3: + # rotate upside down + image = image.rotate(180) + thumbnail, thumbnail_icon = self._process_thumbnail(image, size_reduced) + + logger.debug("...got thumbnail for %s", full_file_name) + return (thumbnail, thumbnail_icon) + + def _get_video_thumbnail(self, full_file_name, size_max, size_reduced): + thumbnail = None + thumbnail_icon = None + if size_max is None: + size = 0 + else: + size = max(size_max[0], size_max[1]) + image = None + if size > 0 and size <= 160: + thm = get_video_THM_file(full_file_name) + if thm: + try: + thumbnail = gtk.gdk.pixbuf_new_from_file(thm) + except: + logger.warning("Could not open THM file for %s", full_file_name) + thumbnail = add_filmstrip(thumbnail) + image = pixbuf_to_image(thumbnail) + + if image is None: + try: + tmp_dir = tempfile.mkdtemp(prefix="rpd-tmp") + thm = os.path.join(tmp_dir, 'thumbnail.jpg') + subprocess.check_call(['ffmpegthumbnailer', '-i', full_file_name, '-t', '10', '-f', '-o', thm, '-s', str(size)]) + image = Image.open(thm) + image.load() + os.unlink(thm) + os.rmdir(tmp_dir) + except: + image = None + logger.error("Error generating thumbnail for %s", full_file_name) + if image: + thumbnail, thumbnail_icon = self._process_thumbnail(image, size_reduced) + + logger.debug("...got thumbnail for %s", full_file_name) + return (thumbnail, thumbnail_icon) + + def get_thumbnail(self, full_file_name, file_type, size_max=None, size_reduced=None): + logger.debug("Getting thumbnail for %s...", full_file_name) + if file_type == rpdfile.FILE_TYPE_PHOTO: + logger.debug("file type is photo") + return self._get_photo_thumbnail(full_file_name, size_max, size_reduced) + else: + return self._get_video_thumbnail(full_file_name, size_max, size_reduced) + + +class GetPreviewImage(multiprocessing.Process): + def __init__(self, results_pipe): + multiprocessing.Process.__init__(self) + self.daemon = True + self.results_pipe = results_pipe + self.thumbnail_maker = Thumbnail() + self.stock_photo_thumbnail_image = None + self.stock_video_thumbnail_image = None + + def get_stock_image(self, file_type): + """ + Get stock image for file type scaled to the current size of the + """ + if file_type == rpdfile.FILE_TYPE_PHOTO: + if self.stock_photo_thumbnail_image is None: + self.stock_photo_thumbnail_image = PicklablePIL(get_stock_photo_image()) + return self.stock_photo_thumbnail_image + else: + if self.stock_video_thumbnail_image is None: + self.stock_video_thumbnail_image = PicklablePIL(get_stock_video_image()) + return self.stock_video_thumbnail_image + + def run(self): + while True: + unique_id, full_file_name, file_type, size_max = self.results_pipe.recv() + full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, file_type, size_max=size_max, size_reduced=None) + if full_size_preview is None: + full_size_preview = self.get_stock_image(file_type) + self.results_pipe.send((unique_id, full_size_preview, reduced_size_preview)) + + + +class GenerateThumbnails(multiprocessing.Process): + def __init__(self, files, batch_size, results_pipe, terminate_queue, + run_event): + multiprocessing.Process.__init__(self) + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.batch_size = batch_size + self.files = files + self.run_event = run_event + self.results = [] + + self.thumbnail_maker = Thumbnail() + + + def run(self): + counter = 0 + i = 0 + for f in self.files: + + # pause if instructed by the caller + self.run_event.wait() + + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + logger.info("Terminating thumbnailing") + return None + + + thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( + f.full_file_name, + f.file_type, + (160, 120), (100,100)) + + self.results.append((f.unique_id, thumbnail_icon, thumbnail)) + counter += 1 + if counter == self.batch_size: + self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results)) + self.results = [] + counter = 0 + i += 1 + + if counter > 0: + # send any remaining results + self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results)) + self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) + self.results_pipe.close() + diff --git a/rapid/utilities.py b/rapid/utilities.py new file mode 100644 index 0000000..07be833 --- /dev/null +++ b/rapid/utilities.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007 - 2011 Damon Lynch <damonlynch@gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import gio +import gtk +from PIL import Image +import distutils.version + +def get_full_path(path): + """ make path relative to home directory if not an absolute path """ + if os.path.isabs(path): + return path + else: + return os.path.join(os.path.expanduser('~'), path) + +def is_directory(path): + + # for some very strange reason, doing it the GIO way fails with + # unknown type, even for directories! + return os.path.isdir(path) + + if False: + d = gio.File(path) + if d.query_exists(): + file_info = d.query_filesystem_info(attributes="standard::type") + file_type = file_info.get_file_type() + if file_type == gio.FILE_TYPE_DIRECTORY: + return True + + return False + +def format_size_for_user(bytes, zero_string="", with_decimals=True, kb_only=False): + """Format an int containing the number of bytes into a string suitable for + printing out to the user. zero_string is the string to use if bytes == 0. + source: https://develop.participatoryculture.org/trac/democracy/browser/trunk/tv/portable/util.py?rev=3993 + + """ + if bytes > (1 << 30) and not kb_only: + value = (bytes / (1024.0 * 1024.0 * 1024.0)) + if with_decimals: + format = "%1.1fGB" + else: + format = "%dGB" + elif bytes > (1 << 20) and not kb_only: + value = (bytes / (1024.0 * 1024.0)) + if with_decimals: + format = "%1.1fMB" + else: + format = "%dMB" + elif bytes > (1 << 10): + value = (bytes / 1024.0) + if with_decimals: + format = "%1.1fKB" + else: + format = "%dKB" + elif bytes > 1: + value = bytes + if with_decimals: + format = "%1.1fB" + else: + format = "%dB" + else: + return zero_string + return format % value + +def register_iconsets(icon_info): + """ + Register icons in the icon set if they're not already used + + From http://faq.pygtk.org/index.py?req=show&file=faq08.012.htp + """ + + icon_factory = gtk.IconFactory() + stock_ids = gtk.stock_list_ids() + for stock_id, file in icon_info: + # only load image files when our stock_id is not present + if stock_id not in stock_ids: + pixbuf = gtk.gdk.pixbuf_new_from_file(file) + iconset = gtk.IconSet(pixbuf) + icon_factory.add(stock_id, iconset) + icon_factory.add_default() + +def escape(s): + """ + Replace special characters by SGML entities. + """ + entities = ("&&", "<<", ">>") + for e in entities: + s = s.replace(e[0], e[1:]) + return s + +def image_to_pixbuf(image): + # convert PIL image to pixbuf + # this one handles transparency, unlike the default example in the pygtk FAQ + # this is also from the pygtk FAQ + IS_RGBA = image.mode=='RGBA' + return gtk.gdk.pixbuf_new_from_data( + image.tostring(), # data + gtk.gdk.COLORSPACE_RGB, # color mode + IS_RGBA, # has alpha + 8, # bits + image.size[0], # width + image.size[1], # height + (IS_RGBA and 4 or 3) * image.size[0] # rowstride + ) + +def pixbuf_to_image(pb): + assert(pb.get_colorspace() == gtk.gdk.COLORSPACE_RGB) + dimensions = pb.get_width(), pb.get_height() + stride = pb.get_rowstride() + pixels = pb.get_pixels() + + mode = pb.get_has_alpha() and "RGBA" or "RGB" + image = Image.frombuffer(mode, dimensions, pixels, + "raw", mode, stride, 1) + + if mode == "RGB": + # convert to having an alpha value, so that the image can + # act as a mask in the drop shadow paste + image = image.convert("RGBA") + + return image + +def pythonify_version(v): + """ makes version number a version number in distutils sense""" + return distutils.version.StrictVersion(v.replace( '~','')) + +def human_readable_version(v): + """ returns a version in human readable form""" + v = v.replace('~a', ' alpha ') + v = v.replace('~b', ' beta ') + return v + |