#!/usr/bin/python # -*- coding: latin1 -*- ### Copyright (C) 2007 - 2011 Damon Lynch ### 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, re 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 QuestionDialog(gtk.Dialog): def __init__(self, parent_window, title, question, use_markup=False, default_to_yes=True, post_choice_callback=None): gtk.Dialog.__init__(self, title, 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(question) prompt_label.set_use_markup(use_markup) 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) if default_to_yes: self.set_default_response(gtk.RESPONSE_OK) else: self.set_default_response(gtk.RESPONSE_CANCEL) self.set_transient_for(parent_window) self.show_all() if post_choice_callback: 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 RemoveAllJobCodeDialog(QuestionDialog): def __init__(self, parent_window, post_choice_callback): QuestionDialog.__init__(self, parent_window, _('Remove all Job Codes?'), _('Should all Job Codes be removed?'), post_choice_callback=post_choice_callback) class RemoveAllRemeberedDevicesDialog(QuestionDialog): def __init__(self, parent_window, post_choice_callback): QuestionDialog.__init__(self, parent_window, _('Remove all Remembered Paths?'), _('Should all remembered paths be removed?'), post_choice_callback=post_choice_callback) class RemoveAllIgnoredPathsDialog(QuestionDialog): def __init__(self, parent_window, post_choice_callback): QuestionDialog.__init__(self, parent_window, _('Remove all Ignored Paths?'), _('Should all ignored paths be removed?'), post_choice_callback=post_choice_callback) 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) self.bump = 16 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 IgnorePathDialog(gtk.Dialog): """ Dialog prompting for a path to ignore when scanning devices""" def __init__(self, parent_window, post_entry_callback): gtk.Dialog.__init__(self, _('Enter a Path to Ignore'), 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_entry_callback = post_entry_callback self.path_entry = gtk.Entry(max=0) self.ignored_path_hbox = gtk.HBox(homogeneous = False) task_label = gtk.Label(_('Specify a path that will never be scanned for photos or videos')) task_label.set_line_wrap(True) task_hbox = gtk.HBox() task_hbox.pack_start(task_label, False, False, padding=6) label = gtk.Label(_('Path:')) self.ignored_path_hbox.pack_start(label, False, False, padding=6) self.ignored_path_hbox.pack_start(self.path_entry, True, True, padding=6) self.set_border_width(6) self.set_has_separator(False) # when user hits enter, close the dialog window self.set_default_response(gtk.RESPONSE_OK) self.path_entry.set_activates_default(True) self.vbox.pack_start(task_hbox, False, False, padding = 6) self.vbox.pack_start(self.ignored_path_hbox, False, False, padding=12) self.set_transient_for(parent_window) self.show_all() self.connect('response', self.on_ignored_path_resp) def on_ignored_path_resp(self, ignored_path_dialog, response): user_chose_path = False if response == gtk.RESPONSE_OK: user_chose_path = True logger.debug("Ignored Path entered") else: logger.debug("Ignored Path not entered") self.post_entry_callback(self, user_chose_path, self.path_entry.get_text()) 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.set_translation_domain(config.APP_NAME) 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() self.file_types = metadatavideo.file_types_to_download() 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_device_options_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_backup_video_folder_filechooser_button_selection_changed(self, widget): self.prefs.backup_video_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 on_add_ignored_path_button_clicked(self, widget): i = IgnorePathDialog(parent_window = self.dialog, post_entry_callback = self.add_ignored_path) def add_ignored_path(self, dialog, user_chose_path, path): dialog.destroy() if user_chose_path: if path and path not in self.prefs.ignored_paths: self.ignored_paths_liststore.prepend((path, )) self.update_ignored_paths() selection = self.ignored_paths_treeview.get_selection() selection.unselect_all() selection.select_path((0, )) #scroll to the top adjustment = self.ignored_paths_scrolledwindow.get_vadjustment() adjustment.set_value(adjustment.lower) def on_ignored_paths_use_re_checkbutton_toggled(self, checkbutton): self.prefs.use_re_ignored_paths = checkbutton.get_active() if self.prefs.use_re_ignored_paths and not self.pref_dialog_startup: # check for invalid regular expressions self.update_ignored_paths() def on_remove_ignored_path_button_clicked(self, button): self._remove_from_treeview(self.ignored_paths_treeview) self.update_ignored_paths() def on_remove_all_ignored_paths_button_clicked(self, button): i = RemoveAllIgnoredPathsDialog(self.dialog, self.remove_all_ignored_paths) def remove_all_ignored_paths(self, dialog, user_selected): dialog.destroy() if user_selected: self.ignored_paths_liststore.clear() self.update_ignored_paths() def on_remove_remembered_device_button_clicked(self, button): """ uses remembered devices treeview to delete any removed items from the device_whitelist and device_blacklist prefs """ blacklist = [i for i in self.prefs.device_blacklist if i] whitelist = [i for i in self.prefs.device_whitelist if i] selection = self.remembered_devices_treeview.get_selection() model, selected = selection.get_selected_rows() iters = [model.get_iter(path) for path in selected] # only delete if a value 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) v = self.remembered_devices_liststore.get_value(iter, 0) if v in blacklist: blacklist.remove(v) elif v in whitelist: whitelist.remove(v) else: logger.debug("Unknown remembered device %s", v) 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.prefs.device_blacklist = blacklist self.prefs.device_whitelist = whitelist def on_remove_all_remembered_device_button_clicked(self, button): r = RemoveAllRemeberedDevicesDialog(self.dialog, self.remove_all_remembered_devices) def remove_all_remembered_devices(self, dialog, user_selected): dialog.destroy() if user_selected: self.remembered_devices_liststore.clear() self.prefs.device_blacklist = [] self.prefs.device_whitelist = [] 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("%s" % 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("%s" % 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_options_tab(self): """ Setup ignored paths and remembered devices tab in prefs dialog """ self.ignored_paths_use_re_checkbutton.set_active( self.prefs.use_re_ignored_paths) self.ignored_paths_liststore = gtk.ListStore(str) column = gtk.TreeViewColumn() rentext = gtk.CellRendererText() rentext.connect('edited', self.on_ignored_path_edited) rentext.set_property('editable', True) column.pack_start(rentext, expand=0) column.set_attributes(rentext, text=0) self.ignored_paths_treeview_column = column self.ignored_paths_treeview.append_column(column) self.ignored_paths_treeview.props.model = self.ignored_paths_liststore for path in self.prefs.ignored_paths: self.ignored_paths_liststore.append((path, )) self.ignored_paths_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.remove_all_ignored_paths_button.set_image(gtk.image_new_from_stock( gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON)) # Remembered devices are a little different in that they cannot be # edited, and they can only added when the user is prompted by the # program. Moreover, the list the user sees is a combination of two # lists: device_whitelist and device_blacklist self.remembered_devices_liststore = gtk.ListStore(str) column = gtk.TreeViewColumn() rentext = gtk.CellRendererText() rentext.set_property('editable', False) column.pack_start(rentext, expand=0) column.set_attributes(rentext, text=0) self.remembered_devices_treeview_column = column self.remembered_devices_treeview.append_column(column) self.remembered_devices_treeview.props.model = self.remembered_devices_liststore for device in self.prefs.device_whitelist: if device: self.remembered_devices_liststore.append((device, )) for device in self.prefs.device_blacklist: if device: self.remembered_devices_liststore.append((device, )) self.remembered_devices_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.remove_all_remembered_device_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): """ Setup and configure backup tab """ #Manual backup location for photos file chooser self.backup_folder_filechooser_button = gtk.FileChooserButton( _("Select a folder in which to backup photos")) 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() #Manual backup location for videos file chooser self.backup_video_folder_filechooser_button = gtk.FileChooserButton( _("Select a folder in which to backup videos")) self.backup_video_folder_filechooser_button.set_current_folder( self.prefs.backup_video_location) self.backup_video_folder_filechooser_button.set_action( gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) self.backup_video_folder_filechooser_button.connect("selection-changed", self.on_backup_video_folder_filechooser_button_selection_changed) self.backup_table.attach(self.backup_video_folder_filechooser_button, 3, 4, 9, 10, yoptions = gtk.SHRINK) self.backup_video_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] if metadatavideo.DOWNLOAD_VIDEO: self._backup_controls2 += [self.backup_video_folder_filechooser_button, self.backup_video_location_label] else: self.backup_video_folder_filechooser_button.set_sensitive(False) self.backup_video_location_label.set_sensitive(False) 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.generate_thumbnails_checkbutton.set_active( self.prefs.generate_thumbnails) self.auto_rotate_checkbutton.set_active( self.prefs.auto_rotate_jpeg) 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 = "%s" % 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 and . These are used to format the text the users sees text += _("Warning: There is insufficient metadata to fully generate the name. Please use other renaming options.") 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 = _("Warning: There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options." ) # 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(_("Example: %s") % 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 _remove_from_treeview(self, treeview): """ Removes selected items from a treeview, allowing multiple selections """ selection = treeview.get_selection() model, selected = selection.get_selected_rows() iters = [model.get_iter(path) for path in selected] # only delete if a value 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,)) def on_remove_job_code_button_clicked(self, button): """ remove selected job codes (can be multiple selection)""" self._remove_from_treeview(self.job_code_treeview) 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_prefs_list(self, liststore): replacement_list = [] for row in liststore: replacement_list.append(row[0]) return replacement_list def update_job_codes(self): """ update preferences with list of job codes""" self.prefs.job_codes = self._update_prefs_list(self.job_code_liststore) def on_ignored_path_edited(self, widget, path, new_text): iter = self.ignored_paths_liststore.get_iter(path) self.ignored_paths_liststore.set_value(iter, 0, new_text) self.update_ignored_paths() def update_ignored_paths(self): ignored_paths = self._update_prefs_list(self.ignored_paths_liststore) # remove any trailing slashes ignored_paths = [path.rstrip('/') for path in ignored_paths if path] # remove any blank values from ignored_paths ignored_paths = [path for path in ignored_paths if path] if self.prefs.use_re_ignored_paths: ip = [] bad_paths = '' for path in ignored_paths: # check for validity try: re.match(path, '') ip.append(path) except: logger.error("Ignoring invalid regular expression: %s", path) bad_paths += path + '\n' ignored_paths = ip if bad_paths: bad_paths = bad_paths[:-1] if bad_paths.find('\n') >= 0: msg = _("The following regular expressions are invalid, and will be removed unless you correct them:\n %s") % bad_paths else: msg = _("This regular expression is invalid, and will be removed unless you correct it:\n %s") % bad_paths misc.run_dialog(_("Invalid regular expression"), msg, self) self.prefs.ignored_paths = ignored_paths 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_rotate_checkbutton_toggled(self, checkbutton): self.prefs.auto_rotate_jpeg = 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_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_generate_thumbnails_checkbutton_toggled(self, checkbutton): self.prefs.generate_thumbnails = 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() 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 = "%s\n%s\n%s" % (path, path2, path3) else: example = "%s\n%s" % (path, path2) self.example_backup_path_label.set_markup(example)