1 #include "tag_entry.hpp"
2 #include "pool/ipool.hpp"
3 #include "util/sqlite.hpp"
4 
5 namespace horizon {
6 
7 class TagEntry::TagPopover : public Gtk::Popover {
8 public:
9     TagPopover(TagEntry *p);
10 
11     TagEntry *parent;
12     Gtk::SearchEntry *search_entry;
13     class ListColumns : public Gtk::TreeModelColumnRecord {
14     public:
ListColumns()15         ListColumns()
16         {
17             Gtk::TreeModelColumnRecord::add(name);
18             Gtk::TreeModelColumnRecord::add(count);
19         }
20         Gtk::TreeModelColumn<Glib::ustring> name;
21         Gtk::TreeModelColumn<int> count;
22     };
23     ListColumns list_columns;
24 
25     Gtk::TreeView *view;
26     Glib::RefPtr<Gtk::ListStore> store;
27 
28     void update_list();
29     void update_list_edit();
30     void activate();
31 };
32 
TagPopover(TagEntry * p)33 TagEntry::TagPopover::TagPopover(TagEntry *p) : Gtk::Popover(), parent(p)
34 {
35     auto box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 3));
36     search_entry = Gtk::manage(new Gtk::SearchEntry());
37     box->pack_start(*search_entry, false, false, 0);
38 
39     store = Gtk::ListStore::create(list_columns);
40 
41 
42     search_entry->signal_changed().connect([this] {
43         std::string search = search_entry->get_text();
44         update_list();
45         if (!parent->edit_mode) {
46             if (store->children().size())
47                 view->get_selection()->select(store->children().begin());
48             auto it = view->get_selection()->get_selected();
49             if (it) {
50                 view->scroll_to_row(store->get_path(it));
51             }
52         }
53     });
54     if (parent->edit_mode) {
55         search_entry->signal_activate().connect([this] {
56             parent->add_tag(search_entry->get_text());
57             search_entry->set_text("");
58             search_entry->grab_focus();
59             update_list();
60         });
61     }
62     else {
63         search_entry->signal_activate().connect(sigc::mem_fun(*this, &TagPopover::activate));
64     }
65 
66     view = Gtk::manage(new Gtk::TreeView(store));
67     view->set_headers_visible(false);
68     view->get_selection()->set_mode(Gtk::SELECTION_BROWSE);
69     view->append_column("Tag", list_columns.name);
70     view->get_column(0)->set_expand(true);
71     if (!parent->edit_mode) {
72         auto cr = Gtk::manage(new Gtk::CellRendererText());
73         auto tvc = Gtk::manage(new Gtk::TreeViewColumn("N", *cr));
74         tvc->add_attribute(cr->property_text(), list_columns.count);
75         cr->property_xalign() = 1;
76         cr->property_sensitive() = false;
77         auto attributes_list = pango_attr_list_new();
78         auto attribute_font_features = pango_attr_font_features_new("tnum 1");
79         pango_attr_list_insert(attributes_list, attribute_font_features);
80         g_object_set(G_OBJECT(cr->gobj()), "attributes", attributes_list, NULL);
81         pango_attr_list_unref(attributes_list);
82         view->append_column(*tvc);
83     }
84     view->set_enable_search(false);
85     view->signal_key_press_event().connect([this](GdkEventKey *ev) -> bool {
86         search_entry->grab_focus_without_selecting();
87         return search_entry->handle_event(ev);
88     });
89 
90     view->signal_row_activated().connect(
91             [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *col) { activate(); });
92 
93     property_visible().signal_changed().connect([this] {
94         if (get_visible()) {
95             search_entry->set_text("");
96             view->get_selection()->unselect_all();
97             update_list();
98         }
99     });
100 
101     auto sc = Gtk::manage(new Gtk::ScrolledWindow());
102     sc->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
103     sc->set_shadow_type(Gtk::SHADOW_IN);
104     sc->set_min_content_height(210);
105     sc->add(*view);
106 
107     box->pack_start(*sc, true, true, 0);
108 
109     add(*box);
110     box->show_all();
111 
112     update_list();
113 }
114 
activate()115 void TagEntry::TagPopover::activate()
116 {
117     auto it = view->get_selection()->get_selected();
118     if (it) {
119         Gtk::TreeModel::Row row = *it;
120         Glib::ustring tag = row[list_columns.name];
121         parent->add_tag(tag);
122         search_entry->set_text("");
123         search_entry->grab_focus();
124         update_list();
125     }
126 }
127 
update_list()128 void TagEntry::TagPopover::update_list()
129 {
130     store->clear();
131     if (parent->edit_mode) {
132         update_list_edit();
133         return;
134     }
135     auto tags_existing = parent->get_tags();
136     auto ntags = tags_existing.size();
137     std::stringstream query;
138     query << "SELECT tag, count(*) AS cnt, tag LIKE $pre AS prefix from tags "
139              "WHERE type = $type "
140              "AND tag LIKE $tag "
141              "AND tag NOT in (";
142     for (size_t i = 0; i < ntags; i++) {
143         query << "$etag" << i << ",";
144     }
145     query << "'') ";
146     if (ntags) {
147         query << "AND uuid IN (SELECT uuid FROM tags WHERE (";
148 
149         for (size_t i = 0; i < ntags; i++) {
150             query << "tag = $etag" << i << " OR ";
151         }
152         query << "0) AND type = $type "
153                  "GROUP by tags.uuid HAVING count(*) >= $ntags) ";
154     }
155     query << "GROUP BY tag "
156              "ORDER BY prefix DESC, cnt DESC";
157     SQLite::Query q(parent->pool.get_db(), query.str());
158     {
159         size_t i = 0;
160         for (const auto &it : tags_existing) {
161             std::string b = "$etag" + std::to_string(i);
162             q.bind(b.c_str(), it);
163             i++;
164         }
165     }
166 
167     q.bind("$pre", search_entry->get_text() + "%");
168     if (ntags) {
169         q.bind("$ntags", ntags);
170     }
171     q.bind("$tag", "%" + search_entry->get_text() + "%");
172     q.bind("$type", parent->type);
173     while (q.step()) {
174         Gtk::TreeModel::Row row = *store->append();
175         row[list_columns.name] = q.get<std::string>(0);
176         row[list_columns.count] = q.get<int>(1);
177     }
178 }
179 
update_list_edit()180 void TagEntry::TagPopover::update_list_edit()
181 {
182     auto tags_existing = parent->get_tags();
183     auto ntags = tags_existing.size();
184     std::stringstream query;
185     query << "WITH ids_existing AS (SELECT uuid FROM tags WHERE type = $type "
186              "AND tag in (";
187     for (size_t i = 0; i < ntags; i++) {
188         query << "$etag" << i << ",";
189     }
190     query << "'') GROUP BY uuid HAVING count(*) >= $ntags) ";
191     query << "SELECT tag, count(*) AS cnt, tag LIKE $pre AS prefix, SUM(uuid IN ids_existing) AS n_ex FROM tags "
192              "WHERE type = $type "
193              "AND tag LIKE $tag "
194              "AND tag NOT in (";
195     for (size_t i = 0; i < ntags; i++) {
196         query << "$etag" << i << ",";
197     }
198     query << "'') ";
199     query << "GROUP BY tag "
200              "ORDER BY prefix DESC, n_ex DESC, cnt DESC";
201     SQLite::Query q(parent->pool.get_db(), query.str());
202     {
203         size_t i = 0;
204         for (const auto &it : tags_existing) {
205             std::string b = "$etag" + std::to_string(i);
206             q.bind(b.c_str(), it);
207             i++;
208         }
209     }
210 
211     q.bind("$pre", search_entry->get_text() + "%");
212     if (ntags) {
213         q.bind("$ntags", ntags);
214     }
215     q.bind("$tag", "%" + search_entry->get_text() + "%");
216     q.bind("$type", parent->type);
217     while (q.step()) {
218         Gtk::TreeModel::Row row = *store->append();
219         row[list_columns.name] = q.get<std::string>(0);
220         row[list_columns.count] = q.get<int>(1);
221     }
222 }
223 
TagEntry(IPool & p,ObjectType t,bool e)224 TagEntry::TagEntry(IPool &p, ObjectType t, bool e)
225     : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4), pool(p), type(t), edit_mode(e)
226 {
227     box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
228     box->show();
229     pack_start(*box, false, false, 0);
230 
231     add_button = Gtk::manage(new Gtk::MenuButton);
232     add_button->set_image_from_icon_name("list-add-symbolic", Gtk::ICON_SIZE_BUTTON);
233     add_button->show();
234     add_button->set_tooltip_text("No more tags available");
235     add_button->set_has_tooltip(false);
236     pack_start(*add_button, false, false, 0);
237 
238     auto popover = Gtk::manage(new TagPopover(this));
239     add_button->set_popover(*popover);
240 }
241 
242 class TagEntry::TagLabel : public Gtk::Box {
243 public:
TagLabel(TagEntry * p,const std::string & t)244     TagLabel(TagEntry *p, const std::string &t) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 2), parent(p), tag(t)
245     {
246         la = Gtk::manage(new Gtk::Label(t));
247         pack_start(*la, false, false, 0);
248         la->show();
249 
250         bu = Gtk::manage(new Gtk::Button);
251         bu->set_image_from_icon_name("window-close-symbolic", Gtk::ICON_SIZE_BUTTON);
252         bu->set_relief(Gtk::RELIEF_NONE);
253         bu->get_style_context()->add_class("tag-entry-tiny-button");
254         bu->get_style_context()->add_class("dim-label");
255         bu->show();
256         bu->signal_clicked().connect([this] { parent->remove_tag(tag); });
257         pack_start(*bu, false, false, 0);
258     }
259 
260 private:
261     TagEntry *parent;
262     const std::string tag;
263     Gtk::Label *la = nullptr;
264     Gtk::Button *bu = nullptr;
265 };
266 
add_tag(const std::string & tag)267 void TagEntry::add_tag(const std::string &tag)
268 {
269     auto w = Gtk::manage(new TagLabel(this, tag));
270     w->show();
271     box->pack_start(*w, false, false, 0);
272     label_widgets.emplace(tag, w);
273     s_signal_changed.emit();
274     update_add_button_sensitivity();
275 }
276 
remove_tag(const std::string & tag)277 void TagEntry::remove_tag(const std::string &tag)
278 {
279     auto w = label_widgets.at(tag);
280     label_widgets.erase(tag);
281     delete w;
282     s_signal_changed.emit();
283     update_add_button_sensitivity();
284 }
285 
get_tags() const286 std::set<std::string> TagEntry::get_tags() const
287 {
288     std::set<std::string> r;
289     for (const auto &it : label_widgets) {
290         r.emplace(it.first);
291     }
292     return r;
293 }
294 
clear()295 void TagEntry::clear()
296 {
297     for (auto w : label_widgets) {
298         delete w.second;
299     }
300     label_widgets.clear();
301     s_signal_changed.emit();
302     update_add_button_sensitivity();
303 }
304 
set_tags(const std::set<std::string> & tags)305 void TagEntry::set_tags(const std::set<std::string> &tags)
306 {
307     for (auto w : label_widgets) {
308         delete w.second;
309     }
310     label_widgets.clear();
311     for (const auto &tag : tags) {
312         auto w = Gtk::manage(new TagLabel(this, tag));
313         w->show();
314         box->pack_start(*w, false, false, 0);
315         label_widgets.emplace(tag, w);
316     }
317 
318     s_signal_changed.emit();
319     update_add_button_sensitivity();
320 }
321 
update_add_button_sensitivity()322 void TagEntry::update_add_button_sensitivity()
323 {
324     if (edit_mode)
325         return;
326     auto tags = get_tags();
327     size_t ntags = tags.size();
328     bool tags_available = true;
329     if (ntags) {
330         std::stringstream query;
331         query << "SELECT tag FROM tags "
332                  "WHERE type = $type "
333                  "AND tag NOT in (";
334         for (size_t i = 0; i < ntags; i++) {
335             query << "$etag" << i << ",";
336         }
337         query << "'') ";
338         query << "AND uuid IN (SELECT uuid FROM tags WHERE (";
339 
340         for (size_t i = 0; i < ntags; i++) {
341             query << "tag = $etag" << i << " OR ";
342         }
343         query << "0) AND type = $type "
344                  "GROUP by tags.uuid HAVING count(*) >= $ntags) ";
345 
346         SQLite::Query q(pool.get_db(), query.str());
347         {
348             size_t i = 0;
349             for (const auto &it : tags) {
350                 std::string b = "$etag" + std::to_string(i);
351                 q.bind(b.c_str(), it);
352                 i++;
353             }
354         }
355         q.bind("$ntags", ntags);
356         q.bind("$type", type);
357         tags_available = q.step();
358     }
359     add_button->set_sensitive(tags_available);
360     add_button->set_has_tooltip(!tags_available);
361     if (!tags_available) {
362         add_button->get_popover()->hide();
363     }
364 }
365 
366 } // namespace horizon
367