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