From 39b3722a11483b0f1322986528e77d01b4c92183 Mon Sep 17 00:00:00 2001 From: Julien Valroff Date: Sun, 15 Nov 2009 08:17:38 +0000 Subject: New beta release --- rapid/ChangeLog | 52 ++++++ rapid/INSTALL | 18 +- rapid/config.py | 2 +- rapid/glade3/rapid.glade | 405 +++++++++++++++++++++++++++++++----------- rapid/metadata.py | 28 ++- rapid/rapid.py | 350 +++++++++++++++++++++++++++++++++--- rapid/renamesubfolderprefs.py | 70 +++++++- 7 files changed, 770 insertions(+), 155 deletions(-) (limited to 'rapid') diff --git a/rapid/ChangeLog b/rapid/ChangeLog index 430aab8..4d38ed2 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,55 @@ +Version 0.1.0 beta 1 +-------------------- + +2009-11-14 + +This code is ready for full release, but given the magnitude of changes, a beta +seems like a good idea, simply to catch any undetected bugs. + +Added a "Job codes" option. Like the "text" option in image and subfolder name +generation, this allows you to specify text that will be placed into the file +and subfolder names. However, unlike the "text" option, which requires that the +text be directly entered via the program preferences, when using the "Job code" +option, the program will prompt for it each time a download begins. + +Made Download button the default button. Hitting enter while the main window +has focus will now start the download. + +Fixed bug #387002: added dependency in Ubuntu packages for librsvg2-common. +Thanks go to user hasp for this fix. + +Fixed bug #478620: problem with corrupted image files. Thanks go to user Katrin +Krieger for tracking this one down. + +Fixed bug #479424: some camera model names do not have numbers, but it still +makes sense to return a shortened name. Thanks go to user Wesley Harp for +highlighting this problem. + +Fixed bug #482831: program no longer crashes when auto-download is off, and a +device is inserted before another download has completed + +Added Czech translation by Tomas Novak. + +Added French translation by Julien Valroff, Michel Ange, and Cenwen. + +Added Hungarian translation by Balazs Oveges and Andras Lorincz. + +Added Slovak translation by Tomas Novak. + +Added Swedish translation by Ulf Urden and Michal Predotka. + +Added dependency on gnome-icon-theme in Ubuntu packages. + +Added additional hour, minute and second options in image renaming and subfolder +creation. Thanks to Art Zemon for the patch. + +Malformed image date time exif values have are minimally checked to see if they +can still be used for subfolder and image renaming. Some software programs seem +to make a mess of them. + +Update man page, including a bug fix by Julien Valroff. + + Version 0.0.10 -------------- diff --git a/rapid/INSTALL b/rapid/INSTALL index 27f9b07..7972a01 100644 --- a/rapid/INSTALL +++ b/rapid/INSTALL @@ -1,14 +1,14 @@ Rapid Photo Downloader depends on the following software: -- GNOME 2.18 -- GTK+ 2.10 -- Python 2.5 -- pygtk 2.10 -- python-gconf 2.18 -- python-glade2 2.10 -- gnome-python 2.10 -- libexiv2 0.15 -- pyexiv2 0.1.1 +- GNOME 2.18 or higher +- GTK+ 2.10 or higher +- Python 2.5 or 2.6 +- pygtk 2.10 or higher +- python-gconf 2.18 or higher +- python-glade2 2.10 or higher +- gnome-python 2.10 or higher +- libexiv2 0.15 or higher +- pyexiv2 0.1.1 or higher To run Rapid Photo Downloader you will need all the software mentioned above. To start from a fairly basic system, I suggest the following: diff --git a/rapid/config.py b/rapid/config.py index 2e59841..7325538 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -15,7 +15,7 @@ ### 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.0.10' +version = '0.1.0~b1' GCONF_KEY="/apps/rapid-photo-downloader" GLADE_FILE = "glade3/rapid.glade" diff --git a/rapid/glade3/rapid.glade b/rapid/glade3/rapid.glade index be01fa4..92c533b 100644 --- a/rapid/glade3/rapid.glade +++ b/rapid/glade3/rapid.glade @@ -589,14 +589,14 @@ - + True - + True 0 12 - <b>Sequence numbers</b> + <b>Sequence Numbers</b> True @@ -605,11 +605,11 @@ - + True 12 - + True 12 @@ -619,7 +619,7 @@ - + True 12 @@ -796,7 +796,7 @@ - + True 0 12 @@ -805,15 +805,15 @@ False - 2 + 3 - + True 12 - + True 12 @@ -825,7 +825,7 @@ True - 9 + 2 2 @@ -854,48 +854,6 @@ 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 @@ -914,7 +872,7 @@ False 12 - 3 + 4 @@ -939,6 +897,211 @@ tab + + + True + 12 + + + True + + + True + 6 + + + True + emblem-photos + + + False + 0 + + + + + True + <span weight="bold" size="x-large">Job Codes</span> + True + + + False + 1 + + + + + 0 + + + + + True + + + False + 1 + + + + + False + False + 0 + + + + + True + + + True + vertical + + + 0 + 12 + <b>Job Codes</b> + True + + + False + 0 + + + + + True + 12 + + + True + + + False + 0 + + + + + True + True + automatic + automatic + in + + + 250 + True + True + False + True + + + + + False + 1 + + + + + True + + + True + 12 + start + + + gtk-add + True + True + True + True + + + + False + False + 0 + + + + + gtk-remove + True + True + True + True + + + + False + False + 1 + + + + + C_lear + True + True + True + True + + + + False + False + 2 + + + + + 0 + + + + + False + 2 + + + + + 1 + + + + + + + + 0 + + + + + 1 + + + + + 2 + + + + + True + Job Codes + + + 3 + False + tab + + True @@ -1187,7 +1350,7 @@ You can download photos from multiple image devices simultaneously. - 3 + 4 @@ -1196,7 +1359,7 @@ You can download photos from multiple image devices simultaneously. Image Devices - 3 + 4 False tab @@ -1505,7 +1668,7 @@ You can download photos from multiple image devices simultaneously. - 4 + 5 @@ -1514,7 +1677,7 @@ You can download photos from multiple image devices simultaneously. Backup - 4 + 5 False tab @@ -1720,7 +1883,7 @@ You can download photos from multiple image devices simultaneously. - 5 + 6 @@ -1730,7 +1893,7 @@ You can download photos from multiple image devices simultaneously. Automation - 5 + 6 False tab @@ -2116,7 +2279,7 @@ You can download photos from multiple image devices simultaneously. - 6 + 7 @@ -2126,7 +2289,7 @@ You can download photos from multiple image devices simultaneously. Error Handling - 6 + 7 False tab @@ -2208,16 +2371,23 @@ Rapid Photo Downloader is distributed in the hope that it will be useful, but WI 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. Damon Lynch <damonlynch@gmail.com> - Martin Egger <martin.egger@gmx.net> + Michel Ange <michelange@wanadoo.fr> +Martin Egger <martin.egger@gmx.net> +Andras Lorincz Jose Luis Navarro <jlnavarro111@gmail.com> +Tomas Novak <kuvaly@seznam.cz> Abel O'Rian <abel.orian@gmail.com> +Balazs Oveges Daniel Paessler <daniel@paessler.org> Michal Predotka <mpredotka@googlemail.com> Luca Reverberi <thereve@gmail.com> Mikko Ruohola <polarfox@polarfox.net> Sergei Sedov <sedov@webmail.perm.ru> Marco Solari <marcosolari@gmail.com> -Julien Valroff <julien@kirya.net> +Ulf Urdén <ulf.urden@purplescout.com> +Julien Valroff <julien@kirya.net> + + rapid-photo-downloader-about.png True @@ -2550,58 +2720,65 @@ Julien Valroff <julien@kirya.net> - - True - - 9 - - - False - 1 - - - - - False - 3 - - - - - True - - - False - 4 - - - - - True - - - + True - + True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 3 - gtk-dialog-warning - 1 + + 9 False - False 0 - + True - 3 - gtk-dialog-error - 1 + + + + True + + + True + vertical + + + False + 0 + + + + + True + 3 + gtk-dialog-error + 1 + + + False + False + 1 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 3 + gtk-dialog-warning + 1 + + + False + False + 2 + + + + False @@ -2610,12 +2787,24 @@ Julien Valroff <julien@kirya.net> + + 1 + False - False - 5 + 3 + + + + + True + vertical + + + False + 4 @@ -2626,7 +2815,7 @@ Julien Valroff <julien@kirya.net> False False - 6 + 5 diff --git a/rapid/metadata.py b/rapid/metadata.py index 8fc6820..1d4b2df 100755 --- a/rapid/metadata.py +++ b/rapid/metadata.py @@ -220,11 +220,13 @@ class MetaData(pyexiv2.Image): Returns missing if the metadata value is not present. - The short format is determined by the first occurence of a digit in the + The short format is determined by the first occurrence of a digit in the camera model, including all alphaNumeric characters before and after - that digit up till a non-alphanumeric character. + that digit up till a non-alphanumeric character, but with these interventions: - Canon "Mark" designations are shortened prior to conversion. + 1. Canon "Mark" designations are shortened prior to conversion. + 2. Names like "Canon EOS DIGITAL REBEL XSi" do not have a number and must + and treated differently (see below) Examples: Canon EOS 300D DIGITAL -> 300D @@ -233,6 +235,20 @@ class MetaData(pyexiv2.Image): NIKON D2X -> D2X NIKON D70 -> D70 X100,D540Z,C310Z -> X100 + Canon EOS DIGITAL REBEL XSi -> XSi + Canon EOS Digital Rebel XS -> XS + Canon EOS Digital Rebel XTi -> XTi + Canon EOS Kiss Digital X -> Digital + Canon EOS Digital Rebel XT -> XT + EOS Kiss Digital -> Digital + Canon Digital IXUS Wireless -> Wireless + Canon Digital IXUS i zoom -> zoom + Canon EOS Kiss Digital N -> N + Canon Digital IXUS IIs -> IIs + IXY Digital L -> L + Digital IXUS i -> i + IXY Digital -> Digital + Digital IXUS -> IXUS The optional includeCharacters allows additional characters to appear before and after the digits. @@ -246,8 +262,7 @@ class MetaData(pyexiv2.Image): includeCharacters = '\-': DSC-P92 -> DSC-P92 - If a digit is not found in the camera model, the full length camera - model is returned. + If a digit is not found in the camera model, the last word is returned. Note: assume exif values are in ENGLISH, regardless of current platform """ @@ -260,7 +275,8 @@ class MetaData(pyexiv2.Image): if r: return r.group("model") else: - return m.strip() + head, space, model = m.strip().rpartition(' ') + return model else: return missing diff --git a/rapid/rapid.py b/rapid/rapid.py index 0dcbb21..0511e07 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -146,6 +146,8 @@ class Queue(tube.Tube): display_queue = Queue() media_collection_treeview = image_hbox = log_dialog = None +job_code = None +need_job_code = False class ThreadManager: _workers = [] @@ -287,6 +289,11 @@ class ThreadManager: if w.downloadStarted and not w.running: yield w + def getWaitingForJobCodeWorkers(self): + for w in self._workers: + if w.waitingForJobCode: + yield w + def getFinishedWorkers(self): for w in self._workers: if self._isFinished(w): @@ -364,6 +371,11 @@ class RapidPreferences(prefs.Preferences): "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')]) } def __init__(self): @@ -434,7 +446,12 @@ class RapidPreferences(prefs.Preferences): self.day_start = "0:0" return 0, 0 - + def getSampleJobCode(self): + if self.job_codes: + return self.job_codes[0] + else: + return '' + class ImageRenameTable(tpm.TablePlusMinus): def __init__(self, parentApp, adjustScrollWindow): @@ -534,8 +551,6 @@ class ImageRenameTable(tpm.TablePlusMinus): def getParentAppPrefs(self): self.prefList = self.parentApp.prefs.image_rename - - def getPrefsFactory(self): self.prefsFactory = rn.ImageRenamePreferences(self.prefList, self, @@ -544,6 +559,12 @@ class ImageRenameTable(tpm.TablePlusMinus): 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() @@ -654,6 +675,7 @@ class PreferencesDialog(gnomeglade.Component): self._setupDownloadFolderTab() self._setupImageRenameTab() self._setupRenameOptionsTab() + self._setupJobCodeTab() self._setupDeviceTab() self._setupBackupTab() self._setupAutomationTab() @@ -768,6 +790,8 @@ class PreferencesDialog(gnomeglade.Component): self.updateImageRenameExample() 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) @@ -785,9 +809,31 @@ class PreferencesDialog(gnomeglade.Component): self.hour_spinbutton.set_value(float(hour)) self.minute_spinbutton.set_value(float(minute)) + #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.clear_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 an image folder")) @@ -889,6 +935,7 @@ class PreferencesDialog(gnomeglade.Component): """ if hasattr(self, 'rename_table'): + self.rename_table.updateExampleJobCode() name, problem = self.rename_table.prefsFactory.generateNameUsingPreferences( self.sampleImage, self.sampleImageName, self.prefs.strip_characters, sequencesPreliminary=False) @@ -901,7 +948,7 @@ class PreferencesDialog(gnomeglade.Component): if problem: text += "\n" # Translators: please do not modify or leave out html formatting tags like and . These are used to format the text the users sees - text += _("Warning: There is insufficient image metatdata to fully generate the name. Please use other renaming options.") + text += _("Warning: There is insufficient image metadata to fully generate the name. Please use other renaming options.") self.new_name_label.set_markup(text) @@ -911,6 +958,7 @@ class PreferencesDialog(gnomeglade.Component): """ if hasattr(self, 'subfolder_table'): + self.subfolder_table.updateExampleJobCode() path, problem = self.subfolder_table.prefsFactory.generateNameUsingPreferences( self.sampleImage, self.sampleImageName, self.prefs.strip_characters) @@ -921,7 +969,7 @@ class PreferencesDialog(gnomeglade.Component): # since this is markup, escape it path = common.escape(text) if problem: - warning = _("Warning: There is insufficient image metatdata to fully generate subfolders. Please use other subfolder naming options." ) + warning = _("Warning: There is insufficient image metadata to fully generate subfolders. Please use other subfolder naming options." ) 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 @@ -993,6 +1041,75 @@ class PreferencesDialog(gnomeglade.Component): self.widget.destroy() + def on_add_job_code_button_clicked(self, button): + j = JobCodeDialog(self.widget, self.prefs.job_codes, None) + if j.run() == gtk.RESPONSE_OK: + self.add_job_code(j.get_job_code()) + j.destroy() + + def add_job_code(self, job_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.updateImageRenameExample() + self.updateDownloadFolderExample() + + def on_clear_job_code_button_clicked(self, button): + self.job_code_liststore.clear() + self.update_job_codes() + self.updateImageRenameExample() + self.updateDownloadFolderExample() + + 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.updateDownloadFolderExample() + + 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() @@ -1150,6 +1267,7 @@ class CopyPhotos(Thread): self.hasStarted = False self.doNotStart = False + self.waitingForJobCode = False self.autoStart = autoStart self.cardMedia = cardMedia @@ -1371,16 +1489,23 @@ class CopyPhotos(Thread): skipImage = True imageMetadata = newName = newFile = path = subfolder = None else: - imageMetadata.readMetadata() - if not imageMetadata.exifKeys() and (needMetaDataToCreateUniqueSubfolderName or - (needMetaDataToCreateUniqueImageName and - not addUniqueIdentifier)): - logError(config.SERIOUS_ERROR, _("Image has no metadata"), - _("Metadata is essential for generating subfolders / image names.\nSource: %s") % image, - IMAGE_SKIPPED) + try: + # this step can fail if the source image is corrupt + imageMetadata.readMetadata() + except: skipImage = True + + if not skipImage: + if not imageMetadata.exifKeys() and (needMetaDataToCreateUniqueSubfolderName or + (needMetaDataToCreateUniqueImageName and + not addUniqueIdentifier)): + skipImage = True + + if skipImage: + logError(config.SERIOUS_ERROR, _("Image has no metadata"), + _("Metadata is essential for generating subfolders / image names.\nSource: %s") % image, + IMAGE_SKIPPED) newName = newFile = path = subfolder = None - else: subfolder, problem = self.subfolderPrefsFactory.generateNameUsingPreferences( imageMetadata, name, @@ -1650,6 +1775,14 @@ class CopyPhotos(Thread): self.running = False self.lock.release() return + elif self.autoStart and need_job_code: + if job_code == None: + self.waitingForJobCode = True + display_queue.put((self.parentApp.getJobCode, ())) + self.running = False + self.lock.acquire() + self.running = True + self.waitingForJobCode = False elif not self.autoStart: # halt thread, waiting to be restarted so download proceeds self.running = False @@ -1669,10 +1802,19 @@ class CopyPhotos(Thread): self.running = False display_queue.close("rw") return - + + self.downloadStarted = True cmd_line(_("Download has started from %s") % self.cardMedia.prettyName(limit=0)) + if need_job_code and job_code == None: + sys.stderr.write(str(self.thread_id ) + ": job code should never be None\n") + self.imageRenamePrefsFactory.setJobCode('unknown-job-code') + self.subfolderPrefsFactory.setJobCode('unknown-job-code') + else: + self.imageRenamePrefsFactory.setJobCode(job_code) + self.subfolderPrefsFactory.setJobCode(job_code) + # Some images may not have metadata (this # is unlikely for images straight out of a # camera, but it is possible for images that have been edited). If @@ -1802,7 +1944,7 @@ class CopyPhotos(Thread): self.lock.release() except thread_error: - sys.stderr.write(self.thread_id + " thread error\n") + sys.stderr.write(str(self.thread_id) + " thread error\n") def quit(self): """ @@ -2012,6 +2154,80 @@ class ImageHBox(gtk.HBox): adjustment.set_value(adjustment.upper) + + +class JobCodeDialog(gtk.Dialog): + """ Dialog prompting for a job code""" + + def __init__(self, parent_window, job_codes, default_job_code, postJobCodeEntryCB, autoStart): + 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-about.png')) + self.postJobCodeEntryCB = postJobCodeEntryCB + self.autoStart = autoStart + + self.combobox = gtk.combo_box_entry_new_text() + for text in job_codes: + self.combobox.append_text(text) + + self.job_code_hbox = gtk.HBox(False) + + label = gtk.Label(_('Job Code:')) + self.job_code_hbox.pack_start(label, 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(self.job_code_hbox, 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].startswith(self.entry.get_text()) + + 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): + userChoseCode = False + if response == gtk.RESPONSE_OK: + userChoseCode = True + cmd_line(_("Job Code entered")) + else: + cmd_line(_("Job Code not entered - download to be cancelled")) + self.postJobCodeEntryCB(self, userChoseCode, self.get_job_code(), self.autoStart) + + class LogDialog(gnomeglade.Component): """ Displays a log of errors, warnings or other information to the user @@ -2041,11 +2257,13 @@ class LogDialog(gnomeglade.Component): 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() self.textbuffer.insert_with_tags(iter, problem +"\n", self.problemTag) - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert(iter, details + "\n") + 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) @@ -2063,6 +2281,7 @@ class LogDialog(gnomeglade.Component): 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 @@ -2106,8 +2325,10 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): 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() if not displayPreferences: displayPreferences = not self.checkPreferencesOnStartup() @@ -2122,6 +2343,9 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): # control sequence numbers and letters global sequences + + # whether we need to prompt for a job code + global need_job_code duplicate_files = {} @@ -2149,7 +2373,7 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): self.startVolumeMonitor() - # set up tree view display + # 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) @@ -2173,11 +2397,16 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): # menus -# self.menu_resequence.set_sensitive(False) self.menu_display_thumbnails.set_active(self.prefs.display_thumbnails) self.menu_clear.set_sensitive(False) + #job code initialization + need_job_code = self.needJobCode() + self.last_chosen_job_code = None + self.prompting_for_job_code = False + + #setup download and backup mediums, initiating scans self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences) #adjust viewport size for displaying media @@ -2186,6 +2415,8 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): height = self.media_collection_viewport.size_request()[1] self.media_collection_scrolledwindow.set_size_request(-1, height) + self.download_button.grab_default() + # for some reason, the grab focus command is not working... unsure why self.download_button.grab_focus() if displayPreferences: @@ -2225,6 +2456,63 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): # misc.run_dialog(title, msg) return prefsOk + def needJobCode(self): + return rn.usesJobCode(self.prefs.image_rename) or rn.usesJobCode(self.prefs.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) + + jcs = self.prefs.job_codes + while code in jcs: + jcs.remove(code) + + self.prefs.job_codes = [code] + jcs + + + + + def _getJobCode(self, postJobCodeEntryCB, autoStart): + 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) + + def getJobCode(self, autoStart=True): + """ called from the copyphotos thread""" + + self._getJobCode(self.gotJobCode, autoStart) + + def gotJobCode(self, dialog, userChoseCode, code, autoStart): + dialog.destroy() + self.prompting_for_job_code = False + + if userChoseCode: + self.assignJobCode(code) + self.last_chosen_job_code = code + if autoStart: + cmd_line(_("Starting downloads that have been waiting for a Job Code")) + for w in workers.getWaitingForJobCodeWorkers(): + w.startStop() + else: + cmd_line(_("Starting downloads")) + self.startDownload() + + + # FIXME: what happens to these workers that are waiting? How will the user start their download? + # check if need to add code to start button + def checkForUpgrade(self, runningVersion): """ Checks if the running version of the program is different from the version recorded in the preferences. @@ -2307,7 +2595,7 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): def usingVolumeMonitor(self): """ - Returns True if programs needs to use gnomevfs volume monitor + Returns True if programs needs to use gio or gnomevfs volume monitor """ return (self.prefs.device_autodetection or @@ -2573,6 +2861,7 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): 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._set_download_button() self.download_button.connect('clicked', self.on_download_button_clicked) self.download_hbutton_box.set_layout(gtk.BUTTONBOX_START) @@ -2596,7 +2885,10 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): self.startTime = None self.totalDownloadSize = self.totalDownloadedSoFar = 0 self.totalDownloadSizeThisRun = self.totalDownloadedSoFarThisRun = 0 - self.timeRemaining.clear() + # there is no need to clear self.timeRemaining, as when each thread is completed, it removes itself + + global job_code + job_code = None def addToTotalDownloadSize(self, size): self.totalDownloadSize += size @@ -2845,12 +3137,18 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): If pause, a click indicates to pause all running downloads. """ if self.download_button_is_download: - self.startDownload() + if need_job_code and job_code == None and not self.prompting_for_job_code: + self.getJobCode(autoStart=False) + else: + self.startDownload() else: self.pauseDownload() def on_preference_changed(self, key, value): - + """ + Called when user changes the program's preferences + """ + if key == 'display_thumbnails': self.set_display_thumbnails(value) elif key == 'show_log_dialog': @@ -2860,11 +3158,17 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): if self.usingVolumeMonitor(): self.startVolumeMonitor() cmd_line("\n" + _("Preferences were changed.")) + self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = False) if is_beta and verbose: print "Current worker status:" workers.printWorkerStatus() + elif key in ['subfolder', 'image_rename']: + global need_job_code + need_job_code = self.needJobCode() + + def on_error_eventbox_button_press_event(self, widget, event): self.prefs.show_log_dialog = True log_dialog.widget.show() diff --git a/rapid/renamesubfolderprefs.py b/rapid/renamesubfolderprefs.py index 9804c59..f28e068 100644 --- a/rapid/renamesubfolderprefs.py +++ b/rapid/renamesubfolderprefs.py @@ -85,6 +85,7 @@ TEXT = 'Text' FILENAME = 'Filename' METADATA = 'Metadata' SEQUENCES = 'Sequences' +JOB_CODE = 'Job code' SEPARATOR = os.sep @@ -161,7 +162,8 @@ LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', 'MMDDYYYY', 'MMDDYY', 'MMDD', 'DDMMYYYY', 'DDMMYY', 'YYYY', 'YY', 'MM', 'DD', - 'HHMMSS', 'HHMM'] + 'HHMMSS', 'HHMM', 'HH-MM-SS', 'HH-MM', 'HH', 'MM', 'SS'] + LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS] @@ -178,6 +180,8 @@ class i18TranslateMeThanks: _('Filename') _('Metadata') _('Sequences') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + _('Job code') _('Image date') _('Today') _('Yesterday') @@ -275,7 +279,17 @@ class i18TranslateMeThanks: _('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') + # 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. @@ -284,7 +298,8 @@ 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', '%H-%M', + '%H', '%M', '%S'] LIST_IMAGE_NUMBER_L2 = [IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, @@ -392,7 +407,7 @@ DICT_SEQUENCE_L1 = { LIST_IMAGE_RENAME_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, - SEQUENCES] + SEQUENCES, JOB_CODE] DICT_IMAGE_RENAME_L0 = { @@ -401,16 +416,18 @@ DICT_IMAGE_RENAME_L0 = { FILENAME: DICT_FILENAME_L1, METADATA: DICT_METADATA_L1, SEQUENCES: DICT_SEQUENCE_L1, + JOB_CODE: None, ORDER_KEY: LIST_IMAGE_RENAME_L0 } -LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, SEPARATOR] +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 } @@ -556,6 +573,14 @@ def upgradePreferencesToCurrent(imageRenamePrefs, subfolderPrefs, previousVers # 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, version=config.version): """Returns true if the passed in preferences are valid""" @@ -748,6 +773,8 @@ class ImageRenamePreferences: self.fileSequenceLock = fileSequenceLock self.sequences = sequences + + self.job_code = '' # derived classes will have their own definitions, do not overwrite if not hasattr(self, "prefsDefnL0"): @@ -771,7 +798,12 @@ class ImageRenamePreferences: v = '' for i in range(0, len(self.prefList), 3): - s = "%s: " % self.prefList[i] + 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]: @@ -781,6 +813,9 @@ class ImageRenamePreferences: return v + def setJobCode(self, job_code): + self.job_code = job_code + def _getDateComponent(self): """ Returns portion of new image / subfolder name based on date time @@ -804,6 +839,23 @@ class ImageRenamePreferences: if d: if self.L2 <> SUBSECONDS: + + if type(d) == type('string'): + # will be a string only if the date time could not be converted in the datetime type + # try to massage badly formed date / times into a valid value + _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: + v = '' + problem = _('Error in date time component. Value %s appears invalid') % '' + return (v, problem) + try: return (d.strftime(convertDateForStrftime(self.L2)), None) except: @@ -1011,7 +1063,7 @@ class ImageRenamePreferences: return self._getStoredSequenceNo() elif self.L1 == SEQUENCE_LETTER: return self._getSequenceLetter() - + def _getComponent(self): try: if self.L0 == DATE_TIME: @@ -1024,6 +1076,8 @@ class ImageRenamePreferences: return self._getMetadataComponent() elif self.L0 == SEQUENCES: return self._getSequencesComponent() + elif self.L0 == JOB_CODE: + return (self.job_code, None) elif self.L0 == SEPARATOR: return (os.sep, None) except: @@ -1197,7 +1251,7 @@ class ImageRenamePreferences: widgets.append(widget1) widgets.append(None) return - elif key == SEPARATOR: + elif key in [SEPARATOR, JOB_CODE]: widgets.append(None) widgets.append(None) return -- cgit v1.2.3