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