1/* 2* Copyright (c) 2011-2013 Yorba Foundation 3* 4* This program is free software; you can redistribute it and/or 5* modify it under the terms of the GNU Lesser General Public 6* License as published by the Free Software Foundation; either 7* version 2.1 of the License, or (at your option) any later version. 8* 9* This program is distributed in the hope that it will be useful, 10* but WITHOUT ANY WARRANTY; without even the implied warranty of 11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12* General Public License for more details. 13* 14* You should have received a copy of the GNU General Public 15* License along with this program; if not, write to the 16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 17* Boston, MA 02110-1301 USA 18*/ 19 20public class Tags.Branch : Sidebar.Branch { 21 private Gee.HashMap<Tag, Tags.SidebarEntry> entry_map = new Gee.HashMap<Tag, Tags.SidebarEntry> (); 22 23 public Branch () { 24 base (new Tags.Grouping (), 25 Sidebar.Branch.Options.HIDE_IF_EMPTY 26 | Sidebar.Branch.Options.AUTO_OPEN_ON_NEW_CHILD 27 | Sidebar.Branch.Options.STARTUP_OPEN_GROUPING, 28 comparator); 29 30 // seed the branch with existing tags 31 on_tags_added_removed (Tag.global.get_all (), null); 32 33 // monitor collection for future events 34 Tag.global.contents_altered.connect (on_tags_added_removed); 35 Tag.global.items_altered.connect (on_tags_altered); 36 } 37 38 ~Branch () { 39 Tag.global.contents_altered.disconnect (on_tags_added_removed); 40 Tag.global.items_altered.disconnect (on_tags_altered); 41 } 42 43 public Tags.SidebarEntry? get_entry_for_tag (Tag tag) { 44 return entry_map.get (tag); 45 } 46 47 private static int comparator (Sidebar.Entry a, Sidebar.Entry b) { 48 if (a == b) 49 return 0; 50 51 return Tag.compare_names (((Tags.SidebarEntry) a).for_tag (), 52 ((Tags.SidebarEntry) b).for_tag ()); 53 } 54 55 private void on_tags_added_removed (Gee.Iterable<DataObject>? added_raw, Gee.Iterable<DataObject>? removed) { 56 // Store the tag whose page we'll eventually want to go to, 57 // since this is lost when a tag is reparented (pruning a currently- 58 // highlighted entry from the tree causes the highlight to go to the library, 59 // and reparenting requires pruning the old location (along with adding the new one)). 60 Tag? restore_point = null; 61 62 if (added_raw != null) { 63 // prepare a collection of tags guaranteed to be sorted; this is critical for 64 // hierarchical tags since it ensures that parent tags must be encountered 65 // before their children 66 Gee.SortedSet<Tag> added = new Gee.TreeSet<Tag> (Tag.compare_names); 67 foreach (DataObject object in added_raw) { 68 Tag tag = (Tag) object; 69 added.add (tag); 70 } 71 72 foreach (Tag tag in added) { 73 // ensure that all parent tags of this tag (if any) already have sidebar 74 // entries 75 Tag? parent_tag = tag.get_hierarchical_parent (); 76 while (parent_tag != null) { 77 if (!entry_map.has_key (parent_tag)) { 78 Tags.SidebarEntry parent_entry = new Tags.SidebarEntry (parent_tag); 79 entry_map.set (parent_tag, parent_entry); 80 } 81 82 parent_tag = parent_tag.get_hierarchical_parent (); 83 84 } 85 86 Tags.SidebarEntry entry = new Tags.SidebarEntry (tag); 87 entry_map.set (tag, entry); 88 89 parent_tag = tag.get_hierarchical_parent (); 90 if (parent_tag != null) { 91 Tags.SidebarEntry parent_entry = entry_map.get (parent_tag); 92 graft (parent_entry, entry); 93 } else { 94 graft (get_root (), entry); 95 } 96 97 // Save the most-recently-processed on tag. During a reparenting, 98 // this will be the only tag processed. 99 restore_point = tag; 100 } 101 } 102 103 if (removed != null) { 104 foreach (DataObject object in removed) { 105 Tag tag = (Tag) object; 106 107 Tags.SidebarEntry? entry = entry_map.get (tag); 108 assert (entry != null); 109 110 bool is_removed = entry_map.unset (tag); 111 assert (is_removed); 112 113 prune (entry); 114 } 115 } 116 } 117 118 private void on_tags_altered (Gee.Map<DataObject, Alteration> altered) { 119 foreach (DataObject object in altered.keys) { 120 if (!altered.get (object).has_detail ("metadata", "name")) 121 continue; 122 123 Tag tag = (Tag) object; 124 Tags.SidebarEntry? entry = entry_map.get (tag); 125 assert (entry != null); 126 127 entry.sidebar_name_changed (tag.get_user_visible_name ()); 128 entry.sidebar_tooltip_changed (tag.get_user_visible_name ()); 129 reorder (entry); 130 } 131 } 132} 133 134public class Tags.Grouping : Sidebar.Grouping, Sidebar.InternalDropTargetEntry, 135 Sidebar.InternalDragSourceEntry, Sidebar.Contextable { 136 private Gtk.Menu? context_menu = null; 137 138 public Grouping () { 139 base (_ ("Tags"), new ThemedIcon (Resources.ICON_TAGS)); 140 } 141 142 public bool internal_drop_received (Gee.List<MediaSource> media) { 143 return true; 144 } 145 146 public bool internal_drop_received_arbitrary (Gtk.SelectionData data) { 147 if (data.get_data_type ().name () == LibraryWindow.TAG_PATH_MIME_TYPE) { 148 string old_tag_path = (string) data.get_data (); 149 assert (Tag.global.exists (old_tag_path)); 150 151 // if this is already a top-level tag, do a short-circuit return 152 if (HierarchicalTagUtilities.enumerate_path_components (old_tag_path).size < 2) 153 return true; 154 155 AppWindow.get_command_manager ().execute ( 156 new ReparentTagCommand (Tag.for_path (old_tag_path), "/")); 157 158 return true; 159 } 160 161 return false; 162 } 163 164 public void prepare_selection_data (Gtk.SelectionData data) { 165 ; 166 } 167 168 public Gtk.Menu? get_sidebar_context_menu (Gdk.EventButton? event) { 169 if (context_menu == null) { 170 context_menu = new Gtk.Menu (); 171 172 var new_tag_menu_item = new Gtk.MenuItem.with_mnemonic (Resources.NEW_CHILD_TAG_SIDEBAR_MENU); 173 new_tag_menu_item.activate.connect (() => on_new_tag); 174 context_menu.add (new_tag_menu_item); 175 context_menu.show_all (); 176 } 177 178 return context_menu; 179 } 180 181 private void on_new_tag () { 182 NewRootTagCommand creation_command = new NewRootTagCommand (); 183 AppWindow.get_command_manager ().execute (creation_command); 184 LibraryWindow.get_app ().rename_tag_in_sidebar (creation_command.get_created_tag ()); 185 } 186} 187 188public class Tags.SidebarEntry : Sidebar.SimplePageEntry, Sidebar.RenameableEntry, 189 Sidebar.DestroyableEntry, Sidebar.InternalDropTargetEntry, Sidebar.ExpandableEntry, 190 Sidebar.InternalDragSourceEntry { 191 private static Icon single_tag_icon; 192 193 private Tag tag; 194 195 class construct { 196 single_tag_icon = new ThemedIcon (Resources.ICON_ONE_TAG); 197 } 198 199 public SidebarEntry (Tag tag) { 200 this.tag = tag; 201 } 202 203 public Tag for_tag () { 204 return tag; 205 } 206 207 public override string get_sidebar_name () { 208 return tag.get_user_visible_name (); 209 } 210 211 public override Icon? get_sidebar_icon () { 212 return single_tag_icon; 213 } 214 215 protected override Page create_page () { 216 return new TagPage (tag); 217 } 218 219 public void rename (string new_name) { 220 string? prepped = Tag.prep_tag_name (new_name); 221 if (prepped == null) 222 return; 223 224 prepped = prepped.replace ("/", ""); 225 226 if (prepped == tag.get_user_visible_name ()) 227 return; 228 229 if (prepped == "") 230 return; 231 232 AppWindow.get_command_manager ().execute (new RenameTagCommand (tag, prepped)); 233 } 234 235 public void destroy_source () { 236 if (Dialogs.confirm_delete_tag (tag)) 237 AppWindow.get_command_manager ().execute (new DeleteTagCommand (tag)); 238 } 239 240 public bool internal_drop_received (Gee.List<MediaSource> media) { 241 AppWindow.get_command_manager ().execute (new TagUntagPhotosCommand (tag, media, media.size, 242 true)); 243 244 return true; 245 } 246 247 public bool internal_drop_received_arbitrary (Gtk.SelectionData data) { 248 if (data.get_data_type ().name () == LibraryWindow.TAG_PATH_MIME_TYPE) { 249 string old_tag_path = (string) data.get_data (); 250 251 // if we're dragging onto ourself, it's a no-op 252 if (old_tag_path == tag.get_path ()) 253 return true; 254 255 // if we're dragging onto one of our children, it's a no-op 256 foreach (string parent_path in HierarchicalTagUtilities.enumerate_parent_paths (tag.get_path ())) { 257 if (parent_path == old_tag_path) 258 return true; 259 } 260 261 assert (Tag.global.exists (old_tag_path)); 262 263 // if we're dragging onto our parent, it's a no-op 264 Tag old_tag = Tag.for_path (old_tag_path); 265 Tag old_tag_parent = old_tag.get_hierarchical_parent (); 266 if (old_tag_parent != null && old_tag_parent.get_path () == tag.get_path ()) 267 return true; 268 269 AppWindow.get_command_manager ().execute ( 270 new ReparentTagCommand (old_tag, tag.get_path ())); 271 272 return true; 273 } 274 275 return false; 276 } 277 278 public Icon? get_sidebar_open_icon () { 279 return single_tag_icon; 280 } 281 282 public Icon? get_sidebar_closed_icon () { 283 return single_tag_icon; 284 } 285 286 public bool expand_on_select () { 287 return false; 288 } 289 290 public void prepare_selection_data (Gtk.SelectionData data) { 291 data.set (Gdk.Atom.intern_static_string (LibraryWindow.TAG_PATH_MIME_TYPE), 0, 292 tag.get_path ().data); 293 } 294} 295