Commit 45899980 authored by Lucas Beeler's avatar Lucas Beeler

Implements basic hierarchical tag support. Closes #1401.

parent b6e52a2e
......@@ -1269,27 +1269,33 @@ public class AddTagsCommand : PageCommand {
private Gee.HashMap<SourceProxy, Gee.ArrayList<MediaSource>> map =
new Gee.HashMap<SourceProxy, Gee.ArrayList<MediaSource>>();
public AddTagsCommand(string[] names, Gee.Collection<MediaSource> sources) {
base (Resources.add_tags_label(names), "");
public AddTagsCommand(string[] paths, Gee.Collection<MediaSource> sources) {
base (Resources.add_tags_label(paths), "");
// load/create the tags here rather than in execute() so that we can merely use the proxy
// to access it ... this is important with the redo() case, where the tags may have been
// created by another proxy elsewhere
foreach (string name in names) {
Tag tag = Tag.for_name(name);
SourceProxy tag_proxy = tag.get_proxy();
foreach (string path in paths) {
Gee.List<string> paths_to_create =
HierarchicalTagUtilities.enumerate_parent_paths(path);
paths_to_create.add(path);
// for each Tag, only attach sources which are not already attached, otherwise undo()
// will not be symmetric
Gee.ArrayList<MediaSource> add_sources = new Gee.ArrayList<MediaSource>();
foreach (MediaSource source in sources) {
if (!tag.contains(source))
add_sources.add(source);
}
if (add_sources.size > 0) {
tag_proxy.broken.connect(on_proxy_broken);
map.set(tag_proxy, add_sources);
foreach (string create_path in paths_to_create) {
Tag tag = Tag.for_path(create_path);
SourceProxy tag_proxy = tag.get_proxy();
// for each Tag, only attach sources which are not already attached, otherwise undo()
// will not be symmetric
Gee.ArrayList<MediaSource> add_sources = new Gee.ArrayList<MediaSource>();
foreach (MediaSource source in sources) {
if (!tag.contains(source))
add_sources.add(source);
}
if (add_sources.size > 0) {
tag_proxy.broken.connect(on_proxy_broken);
map.set(tag_proxy, add_sources);
}
}
}
......@@ -1334,10 +1340,12 @@ public class RenameTagCommand : SimpleProxyableCommand {
private string old_name;
private string new_name;
// NOTE: new_name should be a name, not a path
public RenameTagCommand(Tag tag, string new_name) {
base (tag, Resources.rename_tag_label(tag.get_name(), new_name), tag.get_name());
base (tag, Resources.rename_tag_label(tag.get_user_visible_name(), new_name),
tag.get_name());
old_name = tag.get_name();
old_name = tag.get_user_visible_name();
this.new_name = new_name;
}
......@@ -1353,11 +1361,32 @@ public class RenameTagCommand : SimpleProxyableCommand {
}
public class DeleteTagCommand : SimpleProxyableCommand {
Gee.List<SourceProxy>? recursive_victim_proxies = null;
public DeleteTagCommand(Tag tag) {
base (tag, Resources.delete_tag_label(tag.get_name()), tag.get_name());
base (tag, Resources.delete_tag_label(tag.get_user_visible_name()), tag.get_name());
}
protected override void execute_on_source(DataSource source) {
Tag tag = (Tag) source;
Gee.List<Tag>? recursive_victims = tag.get_hierarchical_children();
// if this tag has no children just destroy it and do a short-circuit return
if (recursive_victims.size == 0) {
Tag.global.destroy_marked(Tag.global.mark(source), false);
return;
}
// okay, this tag has children, so they need to be proxied and deleted as well
recursive_victim_proxies = new Gee.ArrayList<SourceProxy>();
foreach (Tag victim in recursive_victims) {
recursive_victim_proxies.add(victim.get_proxy());
Tag.global.destroy_marked(Tag.global.mark(victim), false);
}
Tag.global.destroy_marked(Tag.global.mark(source), false);
}
......@@ -1365,6 +1394,36 @@ public class DeleteTagCommand : SimpleProxyableCommand {
// merely instantiating the Tag will rehydrate it ... should always work, because the
// undo stack is cleared if the proxy ever breaks
assert(source is Tag);
if (recursive_victim_proxies != null) {
for (int i = recursive_victim_proxies.size - 1; i >= 0; i--) {
DataSource victim_source = recursive_victim_proxies.get(i).get_source();
assert(victim_source is Tag);
}
}
}
}
public class NewChildTagCommand : SimpleProxyableCommand {
Tag? created_child = null;
public NewChildTagCommand(Tag tag) {
base (tag, _("Create Tag"), tag.get_name());
}
protected override void execute_on_source(DataSource source) {
Tag tag = (Tag) source;
created_child = tag.create_new_child();
}
protected override void undo_on_source(DataSource source) {
Tag.global.destroy_marked(Tag.global.mark(created_child), true);
}
public Tag get_created_child() {
assert(created_child != null);
return created_child;
}
}
......@@ -1437,8 +1496,8 @@ public class TagUntagPhotosCommand : SimpleProxyableCommand {
public TagUntagPhotosCommand(Tag tag, Gee.Collection<MediaSource> sources, int count, bool attach) {
base (tag,
attach ? Resources.tag_photos_label(tag.get_name(), count)
: Resources.untag_photos_label(tag.get_name(), count),
attach ? Resources.tag_photos_label(tag.get_user_visible_name(), count)
: Resources.untag_photos_label(tag.get_user_visible_name(), count),
tag.get_name());
this.sources = sources;
......
......@@ -13,7 +13,7 @@ public bool confirm_delete_tag(Tag tag) {
string msg = ngettext(
"This will remove the tag \"%s\" from one photo. Continue?",
"This will remove the tag \"%s\" from %d photos. Continue?",
count).printf(tag.get_name(), count);
count).printf(tag.get_user_visible_name(), count);
return AppWindow.negate_affirm_question(msg, _("_Cancel"), _("_Delete"),
Resources.DELETE_TAG_TITLE);
......@@ -1592,8 +1592,9 @@ public class ModifyTagsDialog : TagsDialog {
// break up by comma-delimiter, prep for use, and separate into list
string[] tag_names = Tag.prep_tag_names(text.split(","));
// TODO: HTags compatibility
foreach (string name in tag_names)
new_tags.add(Tag.for_name(name));
new_tags.add(Tag.for_path(name));
return new_tags;
}
......
......@@ -3916,17 +3916,44 @@ public class LibraryPhotoSourceCollection : MediaSourceCollection {
Gee.HashMultiMap<Tag, LibraryPhoto> map = new Gee.HashMultiMap<Tag, LibraryPhoto>();
foreach (MediaSource media in media_sources) {
LibraryPhoto photo = (LibraryPhoto) media;
PhotoMetadata metadata = photo.get_metadata();
// if any hierarchical tag information is available, process it first. hierarchical tag
// information must be processed first to avoid tag duplication, since most photo
// management applications that support hierarchical tags also "flatten" the
// hierarchical tag information as plain old tags. If a tag name appears as part of
// a hierarchical path, it needs to be excluded from being processed as a flat tag
HierarchicalTagIndex? htag_index = null;
if (metadata.has_hierarchical_keywords()) {
htag_index = HierarchicalTagUtilities.process_hierarchical_import_keywords(
metadata.get_hierarchical_keywords());
}
if (photo.get_import_keywords() != null) {
foreach (string keyword in photo.get_import_keywords()) {
if (htag_index != null && htag_index.is_tag_in_index(keyword))
continue;
string? name = Tag.prep_tag_name(keyword);
if (name != null)
map.set(Tag.for_name(name), photo);
map.set(Tag.for_path(name), photo);
}
}
if (metadata.has_hierarchical_keywords()) {
foreach (string path in htag_index.get_all_paths()) {
string? name = Tag.prep_tag_name(path);
if (name != null)
map.set(Tag.for_path(name), photo);
}
photo.clear_import_keywords();
}
}
foreach (MediaSource media in media_sources) {
LibraryPhoto photo = (LibraryPhoto) media;
photo.clear_import_keywords();
}
foreach (Tag tag in map.get_keys())
tag.attach_many(map.get(tag));
......@@ -4117,11 +4144,11 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
private bool block_thumbnail_generation = false;
private OneShotScheduler thumbnail_scheduler = null;
private Gee.Collection<string>? import_keywords;
private LibraryPhoto(PhotoRow row, Gee.Collection<string>? import_keywords) {
private LibraryPhoto(PhotoRow row) {
base (row);
this.import_keywords = import_keywords;
this.import_keywords = null;
thumbnail_scheduler = new OneShotScheduler("LibraryPhoto", generate_thumbnails);
......@@ -4132,6 +4159,20 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
if ((row.flags & (FLAG_HIDDEN | FLAG_FAVORITE)) != 0)
upgrade_rating_flags(row.flags);
}
private LibraryPhoto.from_import_params(PhotoImportParams import_params) {
base (import_params.row);
this.import_keywords = import_params.keywords;
thumbnail_scheduler = new OneShotScheduler("LibraryPhoto", generate_thumbnails);
// if marked in a state where they're held in an orphanage, rehydrate their backlinks
if ((import_params.row.flags & (FLAG_TRASH | FLAG_OFFLINE)) != 0)
rehydrate_backlinks(global, import_params.row.backlinks);
if ((import_params.row.flags & (FLAG_HIDDEN | FLAG_FAVORITE)) != 0)
upgrade_rating_flags(import_params.row.flags);
}
public static void init(ProgressMonitor? monitor = null) {
global = new LibraryPhotoSourceCollection();
......@@ -4145,7 +4186,7 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
int count = all.size;
for (int ctr = 0; ctr < count; ctr++) {
PhotoRow row = all.get(ctr);
LibraryPhoto photo = new LibraryPhoto(row, null);
LibraryPhoto photo = new LibraryPhoto(row);
uint64 flags = row.flags;
if ((flags & FLAG_TRASH) != 0)
......@@ -4178,7 +4219,7 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
return ImportResult.DATABASE_ERROR;
// create local object but don't add to global until thumbnails generated
photo = new LibraryPhoto(params.row, params.keywords);
photo = new LibraryPhoto.from_import_params(params);
return ImportResult.SUCCESS;
}
......@@ -4301,7 +4342,7 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
PhotoRow dupe_row = PhotoTable.get_instance().get_row(dupe_id);
// build the DataSource for the duplicate
LibraryPhoto dupe = new LibraryPhoto(dupe_row, null);
LibraryPhoto dupe = new LibraryPhoto(dupe_row);
// clone thumbnails
ThumbnailCache.duplicate(this, dupe);
......@@ -4459,10 +4500,40 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
}
protected override void apply_user_metadata_for_reimport(PhotoMetadata metadata) {
HierarchicalTagIndex? new_htag_index = null;
if (metadata.has_hierarchical_keywords()) {
new_htag_index = HierarchicalTagUtilities.process_hierarchical_import_keywords(
metadata.get_hierarchical_keywords());
}
Gee.Collection<string>? keywords = metadata.get_keywords();
if (keywords != null) {
foreach (string keyword in keywords)
Tag.for_name(keyword).attach(this);
foreach (string keyword in keywords) {
if (new_htag_index != null && new_htag_index.is_tag_in_index(keyword))
continue;
string safe_keyword = HierarchicalTagUtilities.make_flat_tag_safe(keyword);
string promoted_keyword = HierarchicalTagUtilities.flat_to_hierarchical(
safe_keyword);
if (Tag.global.exists(safe_keyword)) {
Tag.for_path(safe_keyword).attach(this);
continue;
}
if (Tag.global.exists(promoted_keyword)) {
Tag.for_path(promoted_keyword).attach(this);
continue;
}
Tag.for_path(keyword).attach(this);
}
}
if (new_htag_index != null) {
foreach (string path in new_htag_index.get_all_paths())
Tag.for_path(path).attach(this);
}
}
}
......
......@@ -283,7 +283,7 @@ private class BasicProperties : Properties {
// display the title if a Tag page
if (title == "" && page is TagPage)
title = ((TagPage) page).get_tag().get_name();
title = ((TagPage) page).get_tag().get_user_visible_name();
if (title != "")
add_line(_("Title:"), guarded_markup_escape_text(title));
......
......@@ -307,6 +307,8 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string DELETE_TAG_TITLE = _("Delete Tag");
public const string DELETE_TAG_SIDEBAR_MENU = _("_Delete");
public const string NEW_CHILD_TAG_SIDEBAR_MENU = _("_New");
public string rename_tag_menu(string name) {
return _("Re_name Tag \"%s\"...").printf(name);
}
......
This diff is collapsed.
......@@ -72,13 +72,17 @@ public class AlienDatabaseImportJob : BatchImportJob {
return false;
AlienDatabasePhoto src_photo = import_source.get_photo();
//
// TODO: HTags compatibility
//
// tags
Gee.Collection<AlienDatabaseTag> src_tags = src_photo.get_tags();
foreach (AlienDatabaseTag src_tag in src_tags) {
string? prepped = prepare_input_text(src_tag.get_name(),
PrepareInputTextOptions.DEFAULT, DEFAULT_USER_TEXT_INPUT_LENGTH);
if (prepped != null)
Tag.for_name(prepped).attach(photo);
Tag.for_path(prepped).attach(photo);
}
// event
AlienDatabaseEvent? src_event = src_photo.get_event();
......
......@@ -378,6 +378,10 @@ public abstract class Events.DirectoryEntry : Sidebar.SimplePageEntry, Sidebar.E
public virtual Icon? get_sidebar_closed_icon() {
return Events.Branch.closed_icon;
}
public bool expand_on_select() {
return true;
}
}
public class Events.MasterDirectoryEntry : Events.DirectoryEntry {
......
......@@ -1363,7 +1363,12 @@ public class LibraryWindow : AppWindow {
// Not all pages have sidebar entries
Sidebar.Entry? entry = page_map.get(page);
if (entry != null) {
sidebar_tree.expand_to_entry(entry);
// if the corresponding sidebar entry is an expandable entry and wants to be
// expanded when it's selected, then expand it
Sidebar.ExpandableEntry expandable_entry = entry as Sidebar.ExpandableEntry;
if (expandable_entry != null && expandable_entry.expand_on_select())
sidebar_tree.expand_to_entry(entry);
sidebar_tree.place_cursor(entry, true);
}
......
......@@ -28,6 +28,21 @@ public enum MetadataDomain {
IPTC
}
public struct HierarchicalKeywordField {
public string field_name;
public string path_separator;
public bool wants_leading_separator;
public bool is_writeable;
public HierarchicalKeywordField(string field_name, string path_separator,
bool wants_leading_separator, bool is_writeable) {
this.field_name = field_name;
this.path_separator = path_separator;
this.wants_leading_separator = wants_leading_separator;
this.is_writeable = is_writeable;
}
}
public abstract class PhotoPreview {
private string name;
private Dimensions dimensions;
......@@ -821,6 +836,14 @@ public class PhotoMetadata : MediaMetadata {
"Iptc.Application2.Keywords"
};
private static HierarchicalKeywordField[] HIERARCHICAL_KEYWORD_TAGS = {
// Xmp.lr.hierarchicalSubject should be writeable but isn't due to this bug
// in libexiv2: http://dev.exiv2.org/issues/784
HierarchicalKeywordField("Xmp.lr.hierarchicalSubject", "|", false, false),
HierarchicalKeywordField("Xmp.digiKam.TagsList", "/", false, true),
HierarchicalKeywordField("Xmp.MicrosoftPhoto.LastKeywordXMP", "/", false, true)
};
public Gee.Set<string>? get_keywords(CompareFunc? compare_func = null) {
Gee.Set<string> keywords = null;
foreach (string tag in KEYWORD_TAGS) {
......@@ -828,19 +851,101 @@ public class PhotoMetadata : MediaMetadata {
if (values != null && values.size > 0) {
if (keywords == null)
keywords = create_string_set(compare_func);
keywords.add_all(values);
foreach (string current_value in values)
keywords.add(HierarchicalTagUtilities.make_flat_tag_safe(current_value));
}
}
return (keywords != null && keywords.size > 0) ? keywords : null;
}
private void internal_set_hierarchical_keywords(HierarchicalTagIndex? index) {
if (index == null) {
foreach (HierarchicalKeywordField current_field in HIERARCHICAL_KEYWORD_TAGS)
remove_tag(current_field.field_name);
return;
}
foreach (HierarchicalKeywordField current_field in HIERARCHICAL_KEYWORD_TAGS) {
if (!current_field.is_writeable)
continue;
Gee.Set<string> writeable_set = new Gee.TreeSet<string>();
foreach (string current_path in index.get_all_paths()) {
string writeable_path = current_path.replace(Tag.PATH_SEPARATOR_STRING,
current_field.path_separator);
if (!current_field.wants_leading_separator)
writeable_path = writeable_path.substring(1);
writeable_set.add(writeable_path);
}
set_string_multiple(current_field.field_name, writeable_set);
}
}
public void set_keywords(Gee.Collection<string>? keywords, SetOption option = SetOption.ALL_DOMAINS) {
if (keywords != null)
set_all_string_multiple(KEYWORD_TAGS, keywords, option);
else
HierarchicalTagIndex htag_index = new HierarchicalTagIndex();
Gee.Set<string> flat_keywords = new Gee.TreeSet<string>();
if (keywords != null) {
foreach (string keyword in keywords) {
if (keyword.has_prefix(Tag.PATH_SEPARATOR_STRING)) {
Gee.Collection<string> path_components =
HierarchicalTagUtilities.enumerate_path_components(keyword);
foreach (string component in path_components)
htag_index.add_path(component, keyword);
} else {
flat_keywords.add(keyword);
}
}
flat_keywords.add_all(htag_index.get_all_tags());
}
if (keywords != null) {
set_all_string_multiple(KEYWORD_TAGS, flat_keywords, option);
internal_set_hierarchical_keywords(htag_index);
} else {
remove_tags(KEYWORD_TAGS);
internal_set_hierarchical_keywords(null);
}
}
public bool has_hierarchical_keywords() {
foreach (HierarchicalKeywordField field in HIERARCHICAL_KEYWORD_TAGS) {
Gee.Collection<string>? values = get_string_multiple(field.field_name);
if (values != null && values.size > 0)
return true;
}
return false;
}
public Gee.Set<string> get_hierarchical_keywords() {
assert(has_hierarchical_keywords());
Gee.Set<string> h_keywords = create_string_set(null);
foreach (HierarchicalKeywordField field in HIERARCHICAL_KEYWORD_TAGS) {
Gee.Collection<string>? values = get_string_multiple(field.field_name);
if (values == null || values.size < 1)
continue;
foreach (string current_value in values) {
string? canonicalized = HierarchicalTagUtilities.canonicalize(current_value,
field.path_separator);
if (canonicalized != null)
h_keywords.add(canonicalized);
}
}
return h_keywords;
}
public bool has_orientation() {
......
......@@ -12,7 +12,8 @@ public class Sidebar.Branch : Object {
NONE = 0,
HIDE_IF_EMPTY,
AUTO_OPEN_ON_NEW_CHILD,
STARTUP_EXPAND_TO_FIRST_CHILD;
STARTUP_EXPAND_TO_FIRST_CHILD,
STARTUP_OPEN_GROUPING;
public bool is_hide_if_empty() {
return (this & HIDE_IF_EMPTY) != 0;
......@@ -25,6 +26,10 @@ public class Sidebar.Branch : Object {
public bool is_startup_expand_to_first_child() {
return (this & STARTUP_EXPAND_TO_FIRST_CHILD) != 0;
}
public bool is_startup_open_grouping() {
return (this & STARTUP_OPEN_GROUPING) != 0;
}
}
private class Node {
......@@ -238,6 +243,10 @@ public class Sidebar.Branch : Object {
return options.is_startup_expand_to_first_child();
}
public bool is_startup_open_grouping() {
return options.is_startup_open_grouping();
}
public void graft(Sidebar.Entry parent, Sidebar.Entry entry,
CompareFunc<Sidebar.Entry>? comparator = null) {
assert(map.has_key(parent));
......
......@@ -30,6 +30,8 @@ public interface Sidebar.ExpandableEntry : Sidebar.Entry {
public abstract Icon? get_sidebar_open_icon();
public abstract Icon? get_sidebar_closed_icon();
public abstract bool expand_on_select();
}
public interface Sidebar.SelectableEntry : Sidebar.Entry {
......
......@@ -322,8 +322,12 @@ public class Sidebar.Tree : Gtk.TreeView {
if (branch.get_show_branch()) {
associate_branch(branch);
if (branch.is_startup_expand_to_first_child())
expand_to_first_child(branch.get_root());
if (branch.is_startup_open_grouping())
expand_to_entry(branch.get_root());
}
branch.entry_added.connect(on_branch_entry_added);
......
......@@ -39,6 +39,10 @@ public class Sidebar.Grouping : Object, Sidebar.Entry, Sidebar.ExpandableEntry {
public string to_string() {
return name;
}
public bool expand_on_select() {
return true;
}
}
// An end-node on the sidebar that represents a Page with its page context menu. Additional
......
......@@ -11,7 +11,7 @@ public class Tags.Branch : Sidebar.Branch {
base (new Tags.Grouping(),
Sidebar.Branch.Options.HIDE_IF_EMPTY
| Sidebar.Branch.Options.AUTO_OPEN_ON_NEW_CHILD
| Sidebar.Branch.Options.STARTUP_EXPAND_TO_FIRST_CHILD,
| Sidebar.Branch.Options.STARTUP_OPEN_GROUPING,
comparator);
// seed the branch with existing tags
......@@ -39,15 +39,40 @@ public class Tags.Branch : Sidebar.Branch {
((Tags.SidebarEntry) b).for_tag());
}
private void on_tags_added_removed(Gee.Iterable<DataObject>? added, Gee.Iterable<DataObject>? removed) {
if (added != null) {
foreach (DataObject object in added) {
private void on_tags_added_removed(Gee.Iterable<DataObject>? added_raw, Gee.Iterable<DataObject>? removed) {
if (added_raw != null) {
// prepare a collection of tags guaranteed to be sorted; this is critical for
// hierarchical tags since it ensures that parent tags must be encountered
// before their children
Gee.SortedSet<Tag> added = new Gee.TreeSet<Tag>(Tag.compare_names);
foreach (DataObject object in added_raw) {
Tag tag = (Tag) object;
added.add(tag);
}
foreach (Tag tag in added) {
// ensure that all parent tags of this tag (if any) already have sidebar
// entries
Tag? parent_tag = tag.get_hierarchical_parent();
while (parent_tag != null) {
if (!entry_map.has_key(parent_tag)) {
Tags.SidebarEntry parent_entry = new Tags.SidebarEntry(parent_tag);
entry_map.set(parent_tag, parent_entry);
}
parent_tag = parent_tag.get_hierarchical_parent();
}
Tags.SidebarEntry entry = new Tags.SidebarEntry(tag);
entry_map.set(tag, entry);
graft(get_root(), entry);
parent_tag = tag.get_hierarchical_parent();
if (parent_tag != null) {
Tags.SidebarEntry parent_entry = entry_map.get(parent_tag);
graft(parent_entry, entry);
} else {
graft(get_root(), entry);
}
}
}
......@@ -75,8 +100,8 @@ public class Tags.Branch : Sidebar.Branch {
Tags.SidebarEntry? entry = entry_map.get(tag);
assert(entry != null);
entry.sidebar_name_changed(tag.get_name());
entry.sidebar_tooltip_changed(tag.get_name());
entry.sidebar_name_changed(tag.get_user_visible_name());
entry.sidebar_tooltip_changed(tag.get_user_visible_name());
reorder(entry);
}
}
......@@ -100,7 +125,7 @@ public class Tags.Grouping : Sidebar.Grouping, Sidebar.InternalDropTargetEntry {
}
public class Tags.SidebarEntry : Sidebar.SimplePageEntry, Sidebar.RenameableEntry,
Sidebar.DestroyableEntry, Sidebar.InternalDropTargetEntry {
Sidebar.DestroyableEntry, Sidebar.InternalDropTargetEntry, Sidebar.ExpandableEntry {
private static Icon single_tag_icon;
private Tag tag;
......@@ -122,7 +147,7 @@ public class Tags.SidebarEntry : Sidebar.SimplePageEntry, Sidebar.RenameableEntr
}
public override string get_sidebar_name() {
return tag.get_name();
return tag.get_user_visible_name();
}
public override Icon? get_sidebar_icon() {
......@@ -155,5 +180,17 @@ public class Tags.SidebarEntry : Sidebar.SimplePageEntry, Sidebar.RenameableEntr
return true;
}
public Icon? get_sidebar_open_icon() {
return single_tag_icon;
}
public Icon? get_sidebar_closed_icon() {
return single_tag_icon;
}
public bool expand_on_select() {
return false;
}
}
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
public class HierarchicalTagIndex {
private Gee.Map<string, Gee.Collection<string>> tag_table;
private Gee.SortedSet<string> known_paths;
public HierarchicalTagIndex( ) {
this.tag_table = new Gee.HashMap<string, Gee.ArrayList<string>>();
this.known_paths = new Gee.TreeSet<string>();
}
public void add_path(string tag, string path) {
if (!tag_table.has_key(tag)) {
tag_table.set(tag, new Gee.ArrayList<string>());
}
tag_table.get(tag).add(path);
known_paths.add(path);
}
public Gee.Collection<string> get_all_paths() {
return known_paths;
}