Commit 2be5ac16 authored by Eric Gregory's avatar Eric Gregory

#1579 customizable directory structure

parent 0e6b31b8
......@@ -82,6 +82,18 @@ class AppDirs {
return File.new_for_path(Environment.get_home_dir()).get_child(_("Pictures"));
}
// Library folder + photo folder, based on user's prefered directory pattern.
public static File get_baked_import_dir(time_t tm) {
string? pattern = Config.get_instance().get_directory_pattern();
if (is_string_empty(pattern))
pattern = Config.get_instance().get_directory_pattern_custom();
if (is_string_empty(pattern))
pattern = "%Y" + Path.DIR_SEPARATOR_S + "%m" + Path.DIR_SEPARATOR_S + "%d"; // default
DateTime date = new DateTime.from_unix_utc(tm);
return File.new_for_path(get_import_dir().get_path() + Path.DIR_SEPARATOR_S + date.format(pattern));
}
// Returns true if the File is in or is equal to the library/import directory.
public static bool is_in_import_dir(File file) {
File import_dir = get_import_dir();
......
......@@ -77,6 +77,15 @@ public class Config {
warning("Unable to set GConf value at %s: %s", path, err.message);
}
private bool unset(string path) {
try {
return client.unset(path);
} catch (Error err) {
report_get_error(path, err);
return false;
}
}
private bool get_bool(string path, bool def) {
try {
if (client.get(path) == null)
......@@ -702,4 +711,32 @@ public class Config {
public void set_commit_metadata_to_masters(bool commit_metadata) {
set_bool(BOOL_COMMIT_METADATA_TO_MASTERS, commit_metadata);
}
public string? get_directory_pattern() {
return (get_string(PATH_SHOTWELL + "/files/directory_pattern", null));
}
public bool set_directory_pattern(string s) {
return set_string(PATH_SHOTWELL + "/files/directory_pattern", s);
}
public bool unset_directory_pattern() {
return unset(PATH_SHOTWELL + "/files/directory_pattern");
}
public string get_directory_pattern_custom() {
return (get_string(PATH_SHOTWELL + "/files/directory_pattern_custom", ""));
}
public bool set_directory_pattern_custom(string s) {
return set_string(PATH_SHOTWELL + "/files/directory_pattern_custom", s);
}
public bool get_use_lowercase_filenames() {
return get_bool(PATH_SHOTWELL + "/files/user_lowercase_filenames", false);
}
public void set_use_lowercase_filenames(bool b) {
set_bool(PATH_SHOTWELL + "/files/user_lowercase_filenames", b);
}
}
......@@ -1636,6 +1636,15 @@ public class WelcomeDialog : Gtk.Dialog {
}
public class PreferencesDialog {
private class PathFormat {
public PathFormat(string name, string? pattern) {
this.name = name;
this.pattern = pattern;
}
public string name;
public string? pattern;
}
private static PreferencesDialog preferences_dialog;
private Gtk.Dialog dialog;
private Gtk.Builder builder;
......@@ -1646,7 +1655,15 @@ public class PreferencesDialog {
private SortedList<AppInfo> external_raw_apps;
private SortedList<AppInfo> external_photo_apps;
private Gtk.FileChooserButton library_dir_button;
private Gtk.ComboBox dir_pattern_combo;
private Gtk.Entry dir_pattern_entry;
private Gtk.Label dir_pattern_example;
private bool allow_closing = false;
private string? lib_dir = null;
private Gee.ArrayList<PathFormat> path_formats = new Gee.ArrayList<PathFormat>();
private GLib.DateTime example_date = new GLib.DateTime.local(2009, 3, 10, 18, 16, 11);
private Gtk.CheckButton lowercase;
private Gtk.Button close_button;
private PreferencesDialog() {
builder = AppWindow.create_builder();
......@@ -1667,8 +1684,31 @@ public class PreferencesDialog {
library_dir_button = builder.get_object("library_dir_button") as Gtk.FileChooserButton;
close_button = builder.get_object("close_button") as Gtk.Button;
photo_editor_combo = builder.get_object("external_photo_editor_combo") as Gtk.ComboBox;
raw_editor_combo = builder.get_object("external_raw_editor_combo") as Gtk.ComboBox;
Gtk.Label pattern_help = builder.get_object("pattern_help") as Gtk.Label;
pattern_help.set_markup("<a href=\"" + Resources.DIR_PATTERN_URL + "\">" + _("(Help)") + "</a>");
dir_pattern_combo = new Gtk.ComboBox.text();
Gtk.Alignment dir_choser_align = builder.get_object("dir choser") as Gtk.Alignment;
dir_choser_align.add(dir_pattern_combo);
dir_pattern_entry = builder.get_object("dir_pattern_entry") as Gtk.Entry;
dir_pattern_example = builder.get_object("dynamic example") as Gtk.Label;
add_to_dir_formats(_("Year" + Path.DIR_SEPARATOR_S + "Month" + Path.DIR_SEPARATOR_S + "Day"),
"%Y" + Path.DIR_SEPARATOR_S + "%m" + Path.DIR_SEPARATOR_S + "%d");
add_to_dir_formats(_("Year" + Path.DIR_SEPARATOR_S + "Month"), "%Y" + Path.DIR_SEPARATOR_S + "%m");
add_to_dir_formats(_("Year" + Path.DIR_SEPARATOR_S + "Month-Day"),
"%Y" + Path.DIR_SEPARATOR_S + "%m-%d");
add_to_dir_formats(_("Year-Month-Day"), "%Y-%m-%d");
add_to_dir_formats(_("Custom"), null); // Custom must always be last.
dir_pattern_combo.changed.connect(on_dir_pattern_combo_changed);
dir_pattern_entry.changed.connect(on_dir_pattern_entry_changed);
lowercase = builder.get_object("lowercase") as Gtk.CheckButton;
lowercase.toggled.connect(on_lowercase_toggled);
populate_preference_options();
......@@ -1690,6 +1730,10 @@ public class PreferencesDialog {
populate_app_combo_box(raw_editor_combo, PhotoFileFormat.RAW.get_mime_types(),
Config.get_instance().get_external_raw_app(), out external_raw_apps);
setup_dir_pattern(dir_pattern_combo, dir_pattern_entry);
lowercase.set_active(Config.get_instance().get_use_lowercase_filenames());
}
private void populate_app_combo_box(Gtk.ComboBox combo_box, string[] mime_types,
......@@ -1750,6 +1794,36 @@ public class PreferencesDialog {
combo_box.set_active(current_app);
}
private void setup_dir_pattern(Gtk.ComboBox combo_box, Gtk.Entry entry) {
string? pattern = Config.get_instance().get_directory_pattern();
bool found = false;
if (null != pattern) {
// Locate pre-built text.
int i = 0;
foreach (PathFormat pf in path_formats) {
if (pf.pattern == pattern) {
combo_box.set_active(i);
found = true;
break;
}
i++;
}
} else {
// Custom path.
string? s = Config.get_instance().get_directory_pattern_custom();
if (!is_string_empty(s)) {
combo_box.set_active(path_formats.size - 1); // Assume "custom" is last.
found = true;
}
}
if (!found) {
combo_box.set_active(0);
}
on_dir_pattern_combo_changed();
}
public static void show() {
if (preferences_dialog == null)
preferences_dialog = new PreferencesDialog();
......@@ -1778,17 +1852,29 @@ public class PreferencesDialog {
if (lib_dir != null)
AppDirs.set_import_dir(lib_dir);
PathFormat pf = path_formats.get(dir_pattern_combo.get_active());
if (null == pf.pattern) {
Config.get_instance().set_directory_pattern_custom(dir_pattern_entry.text);
Config.get_instance().unset_directory_pattern();
} else {
Config.get_instance().set_directory_pattern(pf.pattern);
}
}
private bool on_delete() {
commit_on_close();
if (!get_allow_closing())
return true;
commit_on_close();
return dialog.hide_on_delete(); //prevent widgets from getting destroyed
}
private void on_close() {
if (!get_allow_closing())
return;
dialog.hide();
commit_on_close();
}
......@@ -1807,6 +1893,49 @@ public class PreferencesDialog {
return false;
}
private void on_dir_pattern_combo_changed() {
PathFormat pf = path_formats.get(dir_pattern_combo.get_active());
if (null == pf.pattern) {
// Custom format.
string? dir_pattern = Config.get_instance().get_directory_pattern_custom();
if (is_string_empty(dir_pattern))
dir_pattern = "";
dir_pattern_entry.set_text(dir_pattern);
dir_pattern_entry.editable = true;
dir_pattern_entry.sensitive = true;
} else {
dir_pattern_entry.set_text(pf.pattern);
dir_pattern_entry.editable = false;
dir_pattern_entry.sensitive = false;
}
}
private void on_dir_pattern_entry_changed() {
string example = example_date.format(dir_pattern_entry.text);
if (is_string_empty(example) && !is_string_empty(dir_pattern_entry.text)) {
// Invalid pattern.
dir_pattern_example.set_text(_("Invalid pattern"));
dir_pattern_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_ERROR);
dir_pattern_entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, false);
set_allow_closing(false);
} else {
// Valid pattern.
dir_pattern_example.set_text(example);
dir_pattern_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, null);
set_allow_closing(true);
}
}
private void set_allow_closing(bool allow) {
dialog.set_deletable(allow);
close_button.set_sensitive(allow);
allow_closing = allow;
}
private bool get_allow_closing() {
return allow_closing;
}
private void set_background_color(double bg_color_value) {
Config.get_instance().set_bg_color(to_grayscale((uint16) bg_color_value));
......@@ -1851,6 +1980,16 @@ public class PreferencesDialog {
library_dir_button.current_folder_changed.connect(on_current_folder_changed);
return true;
}
private void add_to_dir_formats(string name, string? pattern) {
PathFormat pf = new PathFormat(name, pattern);
path_formats.add(pf);
dir_pattern_combo.append_text(name);
}
private void on_lowercase_toggled() {
Config.get_instance().set_use_lowercase_filenames(lowercase.get_active());
}
}
// This function is used to determine whether or not files should be copied or linked when imported.
......
......@@ -5,7 +5,6 @@
*/
namespace LibraryFiles {
public const int DIRECTORY_DEPTH = 3;
// This method uses global::generate_unique_file_at in order to "claim" a file in the filesystem.
// Thus, when the method returns success a file may exist already, and should be overwritten.
......@@ -24,15 +23,8 @@ public File? generate_unique_file(string basename, PhotoMetadata? metadata, time
timestamp = time_t();
}
Time tm = Time.local(timestamp);
// build a directory tree inside the library, as deep as DIRECTORY_DEPTH:
// yyyy/mm/dd
File dir = AppDirs.get_import_dir();
dir = dir.get_child("%04u".printf(tm.year + 1900));
dir = dir.get_child("%02u".printf(tm.month + 1));
dir = dir.get_child("%02u".printf(tm.day));
// build a directory tree inside the library
File dir = AppDirs.get_baked_import_dir(timestamp);
try {
dir.make_directory_with_parents(null);
} catch (Error err) {
......@@ -42,7 +34,12 @@ public File? generate_unique_file(string basename, PhotoMetadata? metadata, time
// silently ignore not creating a directory that already exists
}
return global::generate_unique_file(dir, basename, out collision);
// Optionally convert to lower-case.
string newbasename = basename;
if (Config.get_instance().get_use_lowercase_filenames())
newbasename = newbasename.down();
return global::generate_unique_file(dir, newbasename, out collision);
}
// This function is thread-safe.
......
......@@ -71,7 +71,7 @@ public abstract class MediaSource : ThumbnailSource {
// inside the user's Pictures directory
if (file.has_prefix(AppDirs.get_import_dir())) {
File parent = file;
for (int depth = 0; depth < LibraryFiles.DIRECTORY_DEPTH; depth++) {
while(!parent.equal(AppDirs.get_import_dir())) {
parent = parent.get_parent();
if (parent == null)
break;
......
......@@ -20,6 +20,7 @@ namespace Resources {
public const string YORBA_URL = "http://www.yorba.org";
public const string WIKI_URL = "http://trac.yorba.org/wiki/Shotwell";
public const string FAQ_URL = "http://trac.yorba.org/wiki/Shotwell/FAQ";
public const string DIR_PATTERN_URL = "http://trac.yorba.org/wiki/Shotwell/DirectoryPattern";
public const string PREFIX = _PREFIX;
......
......@@ -114,7 +114,7 @@
<child>
<object class="GtkTable" id="table1">
<property name="visible">True</property>
<property name="n_rows">9</property>
<property name="n_rows">14</property>
<property name="n_columns">2</property>
<property name="column_spacing">8</property>
<property name="row_spacing">4</property>
......@@ -132,7 +132,7 @@
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox3">
<object class="GtkHBox" id="slider container">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
......@@ -182,7 +182,7 @@
<property name="top_padding">14</property>
<property name="bottom_padding">3</property>
<child>
<object class="GtkLabel" id="label3">
<object class="GtkLabel" id="library location">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Library Location</property>
......@@ -216,12 +216,12 @@
</object>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">7</property>
<property name="bottom_attach">8</property>
<property name="top_attach">12</property>
<property name="bottom_attach">13</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox2">
<object class="GtkHBox" id="buttons: editors">
<property name="visible">True</property>
<child>
<object class="GtkAlignment" id="alignment2">
......@@ -258,8 +258,8 @@
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">8</property>
<property name="bottom_attach">9</property>
<property name="top_attach">13</property>
<property name="bottom_attach">14</property>
</packing>
</child>
<child>
......@@ -287,7 +287,7 @@
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment9">
<object class="GtkAlignment" id="label: metadata">
<property name="visible">True</property>
<property name="top_padding">14</property>
<property name="bottom_padding">3</property>
......@@ -304,12 +304,12 @@
</object>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="top_attach">10</property>
<property name="bottom_attach">11</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment10">
<object class="GtkAlignment" id="label: metadate write">
<property name="visible">True</property>
<property name="left_padding">10</property>
<child>
......@@ -325,12 +325,12 @@
</object>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
<property name="top_attach">11</property>
<property name="bottom_attach">12</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment11">
<object class="GtkAlignment" id="label: display">
<property name="visible">True</property>
<property name="bottom_padding">3</property>
<child>
......@@ -349,7 +349,7 @@
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment12">
<object class="GtkAlignment" id="labels: external editors">
<property name="visible">True</property>
<property name="left_padding">6</property>
<child>
......@@ -386,13 +386,13 @@
</child>
</object>
<packing>
<property name="top_attach">8</property>
<property name="bottom_attach">9</property>
<property name="top_attach">13</property>
<property name="bottom_attach">14</property>
<property name="x_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment13">
<object class="GtkAlignment" id="label: import to">
<property name="visible">True</property>
<property name="left_padding">10</property>
<child>
......@@ -413,7 +413,7 @@
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment14">
<object class="GtkAlignment" id="label: background">
<property name="visible">True</property>
<child>
<object class="GtkLabel" id="bg_color_label">
......@@ -432,6 +432,174 @@
<property name="x_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="label: importing">
<property name="visible">True</property>
<property name="top_padding">14</property>
<property name="bottom_padding">3</property>
<child>
<object class="GtkLabel" id="importing">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Importing</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="label: directory structure">
<property name="visible">True</property>
<property name="left_padding">10</property>
<child>
<object class="GtkLabel" id="label12">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">_Directory structure:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">library_dir_button</property>
</object>
</child>
</object>
<packing>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="dir choser">
<property name="visible">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="label: patern">
<property name="visible">True</property>
<property name="left_padding">34</property>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<child>
<object class="GtkLabel" id="patern">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">_Pattern:</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="pattern_help">
<property name="visible">True</property>
<attributes>
<attribute name="underline" value="True"/>
</attributes>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="top_attach">7</property>
<property name="bottom_attach">8</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="entry: pattern">
<property name="visible">True</property>
<child>
<object class="GtkEntry" id="dir_pattern_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char"></property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">7</property>
<property name="bottom_attach">8</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="label: dynamic example">
<property name="visible">True</property>
<child>
<object class="GtkLabel" id="dynamic example">
<property name="visible">True</property>
<property name="xalign">0</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">8</property>
<property name="bottom_attach">9</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="label: example">
<property name="visible">True</property>
<property name="left_padding">34</property>
<child>
<object class="GtkLabel" id="example">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Example:</property>
</object>
</child>
</object>
<packing>
<property name="top_attach">8</property>
<property name="bottom_attach">9</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="checkbox: lowercase">
<property name="visible">True</property>
<property name="top_padding">2</property>
<property name="left_padding">10</property>
<child>
<object class="GtkCheckButton" id="lowercase">
<property name="label" translatable="yes">R_ename imported files to lowercase</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
</object>
</child>
</object>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">9</property>
<property name="bottom_attach">10</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
</object>
</child>
</object>
......@@ -445,7 +613,7 @@
<property name="visible">True</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button3">
<object class="GtkButton" id="close_button">
<property name="label">gtk-close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
......@@ -470,7 +638,7 @@
</object>
</child>
<action-widgets>
<action-widget response="-5">button3</action-widget>
<action-widget response="-5">close_button</action-widget>
</action-widgets>
</object>
<object class="GtkAdjustment" id="bg_color_adjustment">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment