1 #include "pool_browser_parametric.hpp"
2 #include "pool/ipool.hpp"
3 #include "pool/part.hpp"
4 #include "util/util.hpp"
5 #include "util/geom_util.hpp"
6 
7 namespace horizon {
8 
9 
create_tvc(const PoolParametric::Column & col,const Gtk::TreeModelColumn<std::string> & tree_col)10 static Gtk::TreeViewColumn *create_tvc(const PoolParametric::Column &col,
11                                        const Gtk::TreeModelColumn<std::string> &tree_col)
12 {
13     if (col.type == PoolParametric::Column::Type::QUANTITY) {
14         auto tvc = Gtk::manage(new Gtk::TreeViewColumn(col.display_name));
15         auto cr_val = Gtk::manage(new Gtk::CellRendererText());
16         auto cr_unit = Gtk::manage(new Gtk::CellRendererText());
17         tvc->set_cell_data_func(*cr_val, [&tree_col](Gtk::CellRenderer *tcr, const Gtk::TreeModel::iterator &it) {
18             Gtk::TreeModel::Row row = *it;
19             auto mcr = dynamic_cast<Gtk::CellRendererText *>(tcr);
20             std::string v = row[tree_col];
21             auto pos = v.find(' ');
22             if (pos == std::string::npos)
23                 mcr->property_text() = v;
24             else
25                 mcr->property_text() = v.substr(0, pos);
26         });
27         tvc->set_cell_data_func(*cr_unit, [&tree_col](Gtk::CellRenderer *tcr, const Gtk::TreeModel::iterator &it) {
28             Gtk::TreeModel::Row row = *it;
29             auto mcr = dynamic_cast<Gtk::CellRendererText *>(tcr);
30             std::string v = row[tree_col];
31             auto pos = v.find(' ');
32             if (pos == std::string::npos)
33                 mcr->property_text() = "";
34             else
35                 mcr->property_text() = v.substr(pos + 1);
36         });
37         cr_val->property_xalign() = 1;
38         cr_unit->property_xalign() = 1;
39         auto attributes_list = pango_attr_list_new();
40         auto attribute_font_features = pango_attr_font_features_new("tnum 1");
41         pango_attr_list_insert(attributes_list, attribute_font_features);
42         g_object_set(G_OBJECT(cr_val->gobj()), "attributes", attributes_list, NULL);
43         pango_attr_list_unref(attributes_list);
44         tvc->pack_start(*cr_val, false);
45         tvc->pack_start(*cr_unit, false);
46         {
47             auto cr_empty = Gtk::manage(new Gtk::CellRendererText());
48             tvc->pack_start(*cr_empty, true);
49         }
50         return tvc;
51     }
52     else {
53         return Gtk::manage(new Gtk::TreeViewColumn(col.display_name, tree_col));
54     }
55 }
56 
string_to_double(const std::string & s)57 static double string_to_double(const std::string &s)
58 {
59     double d;
60     std::istringstream istr(s);
61     istr.imbue(std::locale::classic());
62     istr >> d;
63     return d;
64 }
65 
66 class ParametricFilterBox : public Gtk::Box {
67 public:
ParametricFilterBox(const PoolParametric::Column & col)68     ParametricFilterBox(const PoolParametric::Column &col) : Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4), column(col)
69     {
70         store = Gtk::ListStore::create(list_columns);
71         if (col.type == PoolParametric::Column::Type::QUANTITY) {
72             store->set_sort_func(list_columns.value,
73                                  [this](const Gtk::TreeModel::iterator &ia, const Gtk::TreeModel::iterator &ib) {
74                                      Gtk::TreeModel::Row ra = *ia;
75                                      Gtk::TreeModel::Row rb = *ib;
76                                      std::string a = ra[list_columns.value];
77                                      std::string b = rb[list_columns.value];
78                                      if (a.size() == 0)
79                                          return -1;
80                                      else if (b.size() == 0)
81                                          return 1;
82                                      auto d = string_to_double(a) - string_to_double(b);
83                                      return sgn(d);
84                                  });
85         }
86         else {
87             store->set_sort_func(list_columns.value,
88                                  [this](const Gtk::TreeModel::iterator &ia, const Gtk::TreeModel::iterator &ib) {
89                                      Gtk::TreeModel::Row ra = *ia;
90                                      Gtk::TreeModel::Row rb = *ib;
91                                      std::string a = ra[list_columns.value];
92                                      std::string b = rb[list_columns.value];
93                                      if (a.size() == 0)
94                                          return -1;
95                                      else if (b.size() == 0)
96                                          return 1;
97                                      return strcmp_natural(a, b);
98                                  });
99         }
100         store->set_sort_column(list_columns.value, Gtk::SORT_ASCENDING);
101         view = Gtk::manage(new Gtk::TreeView(store));
102         auto tvc = create_tvc(col, list_columns.value_formatted);
103         view->append_column(*tvc);
104         view->get_column(0)->set_sort_column(list_columns.value);
105         view->get_selection()->set_mode(Gtk::SELECTION_MULTIPLE);
106         view->set_rubber_banding(true);
107         view->show();
108         view->signal_row_activated().connect(
109                 [this](const Gtk::TreeModel::Path &, Gtk::TreeViewColumn *) { s_signal_activated.emit(); });
110         if (col.type == PoolParametric::Column::Type::QUANTITY) {
111             view->set_search_equal_func([this](const Glib::RefPtr<Gtk::TreeModel> &model, int c,
112                                                const Glib::ustring &needle, const Gtk::TreeModel::iterator &it) {
113                 auto v = string_to_double(it->get_value(list_columns.value));
114                 auto needle_f = parse_si(needle);
115                 if (std::isnan(needle_f))
116                     return true;
117                 if (std::abs(needle_f) >= 1 && std::abs(needle_f) < 1000) { // ignore si prefix
118                     int exp = 0;
119                     while (v >= 1e3 && exp <= 12) {
120                         v /= 1e3;
121                         exp += 3;
122                     }
123                     if (v > 1e-15) {
124                         while (v < 1 && exp >= -12) {
125                             v *= 1e3;
126                             exp -= 3;
127                         }
128                     }
129                 }
130 
131                 if (std::abs((v - needle_f) / v) < 0.001)
132                     return false;
133                 else
134                     return true;
135             });
136         }
137         auto sc = Gtk::manage(new Gtk::ScrolledWindow());
138         sc->set_shadow_type(Gtk::SHADOW_IN);
139         sc->set_min_content_height(150);
140         sc->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
141         sc->add(*view);
142         sc->show_all();
143 
144         pack_start(*sc, true, true, 0);
145     }
146 
update(const std::set<std::string> & values)147     void update(const std::set<std::string> &values)
148     {
149         set_visible(values.size() > 1);
150         std::set<std::string> values_selected;
151         for (const auto &path : view->get_selection()->get_selected_rows()) {
152             auto it = store->get_iter(path);
153             Gtk::TreeModel::Row row = *it;
154             values_selected.insert(row[list_columns.value]);
155         }
156         store->clear();
157         for (const auto &value : values) {
158             Gtk::TreeIter it;
159             gtk_list_store_insert_with_values(store->gobj(), it.gobj(), -1, list_columns.value.index(), value.c_str(),
160                                               list_columns.value_formatted.index(), column.format(value).c_str(), -1);
161             if (values_selected.count(value))
162                 view->get_selection()->select(it);
163         }
164     }
165 
get_values()166     std::set<std::string> get_values()
167     {
168         std::set<std::string> r;
169         auto sel = view->get_selection();
170         for (auto &path : sel->get_selected_rows()) {
171             auto it = store->get_iter(path);
172             Gtk::TreeModel::Row row = *it;
173             r.emplace(row[list_columns.value]);
174         }
175         return r;
176     }
177 
reset()178     void reset()
179     {
180         view->get_selection()->unselect_all();
181     }
182 
183     typedef sigc::signal<void> type_signal_activated;
signal_activated()184     type_signal_activated signal_activated()
185     {
186         return s_signal_activated;
187     }
188 
189 private:
190     const PoolParametric::Column &column;
191     Gtk::TreeView *view = nullptr;
192     class ListColumns : public Gtk::TreeModelColumnRecord {
193     public:
ListColumns()194         ListColumns()
195         {
196             Gtk::TreeModelColumnRecord::add(value);
197             Gtk::TreeModelColumnRecord::add(value_formatted);
198         }
199         Gtk::TreeModelColumn<std::string> value;
200         Gtk::TreeModelColumn<std::string> value_formatted;
201     };
202     ListColumns list_columns;
203 
204     Glib::RefPtr<Gtk::ListStore> store;
205     type_signal_activated s_signal_activated;
206 };
207 
PoolBrowserParametric(IPool & p,PoolParametric & pp,const std::string & table_name,const std::string & instance)208 PoolBrowserParametric::PoolBrowserParametric(IPool &p, PoolParametric &pp, const std::string &table_name,
209                                              const std::string &instance)
210     : PoolBrowserStockinfo(p, TreeViewStateStore::get_prefix(instance, "pool_browser_parametric_" + table_name)),
211       pool_parametric(pp), table(pp.get_tables().at(table_name)), list_columns(table)
212 {
213     search_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 10));
214     search_box->property_margin() = 10;
215 
216     auto filters_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 10));
217 
218     auto add_filter_col = [this, &filters_box](auto &col) {
219         auto fbox = Gtk::manage(new ParametricFilterBox(col));
220         fbox->signal_activated().connect([this] {
221             apply_filters();
222             search();
223         });
224         fbox->show();
225         fbox->set_no_show_all(true);
226         filters_box->pack_start(*fbox, false, true, 0);
227         filter_boxes.emplace(col.name, fbox);
228         columns.emplace(col.name, col);
229     };
230 
231     for (const auto &col : pool_parametric.get_extra_columns()) {
232         add_filter_col(col);
233     }
234 
235     for (const auto &col : table.columns) {
236         add_filter_col(col);
237     }
238 
239     filters_box->show();
240     search_box->pack_start(*filters_box, false, false, 0);
241 
242 
243     auto hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 10));
244 
245     auto search_button = Gtk::manage(new Gtk::Button("Search"));
246     search_button->signal_clicked().connect([this] {
247         apply_filters();
248         search();
249     });
250     search_button->set_halign(Gtk::ALIGN_START);
251     hbox->pack_start(*search_button, false, false, 0);
252     search_button->show();
253 
254     auto reset_button = Gtk::manage(new Gtk::Button("Reset"));
255     reset_button->signal_clicked().connect([this] {
256         for (auto &it : filter_boxes) {
257             it.second->reset();
258         }
259         filters_applied.clear();
260         search();
261     });
262     reset_button->set_halign(Gtk::ALIGN_START);
263     hbox->pack_start(*reset_button, false, false, 0);
264     reset_button->show();
265 
266     filters_applied_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 5));
267     filters_applied_box->show();
268     hbox->pack_start(*filters_applied_box, true, true, 0);
269 
270     if (auto selector = create_pool_selector()) {
271         hbox->pack_start(*selector, false, false, 0);
272         selector->show();
273     }
274 
275     hbox->show();
276     search_box->pack_start(*hbox, false, false, 0);
277 
278     construct(search_box);
279     scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
280     install_pool_item_source_tooltip();
281     update_filters();
282 }
283 
create_list_store()284 Glib::RefPtr<Gtk::ListStore> PoolBrowserParametric::create_list_store()
285 {
286     return Gtk::ListStore::create(list_columns);
287 }
288 
289 
create_columns()290 void PoolBrowserParametric::create_columns()
291 {
292     {
293         auto col = append_column_with_item_source_cr("MPN", list_columns.MPN, Pango::ELLIPSIZE_END);
294         col->set_resizable(true);
295         col->set_min_width(100);
296     }
297     {
298         auto col = append_column("Manufacturer", list_columns.manufacturer, Pango::ELLIPSIZE_END);
299         col->set_resizable(true);
300         col->set_min_width(100);
301     }
302     {
303         auto col = append_column("Package", list_columns.package, Pango::ELLIPSIZE_END);
304         col->set_resizable(true);
305         col->set_min_width(100);
306     }
307     for (const auto &col : table.columns) {
308         auto tvc = create_tvc(col, list_columns.params.at(col.name));
309         treeview->append_column(*tvc);
310     }
311 }
312 
add_sort_controller_columns()313 void PoolBrowserParametric::add_sort_controller_columns()
314 {
315     sort_controller->add_column(0, "parts.MPN");
316     sort_controller->add_column(1, "parts.manufacturer");
317     sort_controller->add_column(2, "packages.name");
318     for (size_t i = 0; i < table.columns.size(); i++) {
319         auto &col = table.columns.at(i);
320         sort_controller->add_column(3 + i, "p." + col.name);
321     }
322 }
323 
get_in(const std::string & prefix,size_t n)324 static std::string get_in(const std::string &prefix, size_t n)
325 {
326     std::string s = "(";
327     for (size_t i = 0; i < n - 1; i++) {
328         s += "$" + prefix + std::to_string(i) + ", ";
329     }
330     s += "$" + prefix + std::to_string(n - 1) + ") ";
331     return s;
332 }
333 
bind_set(SQLite::Query & q,const std::string & prefix,const std::set<std::string> & values)334 static void bind_set(SQLite::Query &q, const std::string &prefix, const std::set<std::string> &values)
335 {
336     size_t i = 0;
337     for (const auto &v : values) {
338         q.bind(("$" + prefix + std::to_string(i)).c_str(), v);
339         i++;
340     }
341 }
342 
apply_filters()343 void PoolBrowserParametric::apply_filters()
344 {
345     for (auto &it : filter_boxes) {
346         auto values = it.second->get_values();
347         if (values.size())
348             filters_applied[it.first] = values;
349         it.second->reset();
350     }
351 }
352 
search()353 void PoolBrowserParametric::search()
354 {
355     prepare_search();
356     values_remaining.clear();
357     iter_cache.clear();
358 
359     std::set<std::string> manufacturers;
360     if (filters_applied.count("_manufacturer"))
361         manufacturers = filters_applied.at("_manufacturer");
362     std::set<std::string> packages;
363     if (filters_applied.count("_package"))
364         packages = filters_applied.at("_package");
365     std::string qs;
366     if (similar_part_uuid) {
367         qs += "WITH RECURSIVE all_bases(uuidx) AS (SELECT $similar_part UNION "
368               "SELECT parts.base FROM parts INNER JOIN all_bases ON parts.uuid = uuidx "
369               "WHERE parts.base != '00000000-0000-0000-0000-000000000000'), "
370               "all_derived(uuidy) AS (SELECT * FROM all_bases UNION "
371               "SELECT parts.uuid FROM parts INNER JOIN all_derived ON parts.base = uuidy), "
372               "real_bases(uuidz) AS (SELECT DISTINCT parts.base FROM parts INNER JOIN all_derived ON "
373               "all_derived.uuidy = parts.base) ";
374     }
375     qs += "SELECT p.*, parts.MPN, parts.manufacturer, packages.name, parts.filename, parts.pool_uuid, "
376           "parts.last_pool_uuid "
377           "FROM "
378           + table.name
379           + " AS p LEFT JOIN pool.parts USING (uuid) LEFT JOIN pool.packages ON parts.package = packages.uuid ";
380     if (similar_part_uuid) {
381         qs += "INNER JOIN real_bases ON real_bases.uuidz = parts.base ";
382     }
383     qs += "WHERE 1 ";
384     if (manufacturers.size()) {
385         qs += " AND parts.manufacturer IN " + get_in("_manufacturer", manufacturers.size());
386     }
387     if (packages.size()) {
388         qs += " AND packages.name IN " + get_in("_package", packages.size());
389     }
390     for (const auto &it : filters_applied) {
391         if (it.first.at(0) != '_') {
392             if (it.second.size()) {
393                 qs += " AND p." + it.first + " IN " + get_in(it.first, it.second.size());
394             }
395         }
396     }
397     qs += get_pool_selector_query();
398     qs += sort_controller->get_order_by();
399     SQLite::Query q(pool_parametric.db, qs);
400     bind_set(q, "_manufacturer", manufacturers);
401     bind_set(q, "_package", packages);
402     bind_pool_selector_query(q);
403     if (similar_part_uuid) {
404         q.bind("$similar_part", similar_part_uuid);
405     }
406     for (const auto &it : filters_applied) {
407         if (it.first.at(0) != '_') {
408             bind_set(q, it.first, it.second);
409         }
410     }
411     std::list<UUID> uuids;
412     try {
413         Gtk::TreeModel::Row row;
414         while (q.step()) {
415             UUID uu(q.get<std::string>(0));
416             uuids.push_back(uu);
417             auto iter = store->append();
418             row = *(iter);
419             row[list_columns.uuid] = uu;
420             iter_cache.emplace(uu, iter);
421             for (size_t i = 0; i < table.columns.size(); i++) {
422                 auto &col = table.columns.at(i);
423                 std::string v = q.get<std::string>(1 + i);
424                 row[list_columns.params.at(col.name)] = col.format(v);
425                 values_remaining[col.name].emplace(v);
426             }
427             size_t ofs = table.columns.size() + 1;
428             row[list_columns.MPN] = q.get<std::string>(ofs + 0);
429             std::string manufacturer = q.get<std::string>(ofs + 1);
430             std::string package = q.get<std::string>(ofs + 2);
431             row[list_columns.path] = q.get<std::string>(ofs + 3);
432             row[list_columns.source] = pool_item_source_from_db(q, ofs + 4, ofs + 5);
433             row[list_columns.manufacturer] = manufacturer;
434             row[list_columns.package] = package;
435             values_remaining["_manufacturer"].emplace(manufacturer);
436             values_remaining["_package"].emplace(package);
437         }
438         set_busy(false);
439     }
440     catch (SQLite::Error &e) {
441         if (e.rc == SQLITE_BUSY) {
442             set_busy(true);
443         }
444         else {
445             throw;
446         }
447     }
448 
449     finish_search();
450     update_filters();
451     update_filters_applied();
452     if (stock_info_provider)
453         stock_info_provider->update_parts(uuids);
454 }
455 
update_filters()456 void PoolBrowserParametric::update_filters()
457 {
458     for (auto &it : filter_boxes) {
459         if (values_remaining.count(it.first)) {
460             const auto &values = values_remaining.at(it.first);
461             it.second->update(values);
462         }
463         else {
464             it.second->set_visible(false);
465         }
466     }
467 }
468 
469 
470 class PoolBrowserParametric::FilterAppliedLabel : public Gtk::Box {
471 public:
FilterAppliedLabel(PoolBrowserParametric * p,const PoolParametric::Column & c,const std::set<std::string> & values)472     FilterAppliedLabel(PoolBrowserParametric *p, const PoolParametric::Column &c, const std::set<std::string> &values)
473         : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 2), parent(p), column(c)
474     {
475         la = Gtk::manage(new Gtk::Label(column.display_name + " (" + std::to_string(values.size()) + ")"));
476         pack_start(*la, false, false, 0);
477         la->show();
478         std::string tooltip;
479         for (const auto &it : values) {
480             tooltip += column.format(it) + "\n";
481         }
482         if (tooltip.size())
483             tooltip.pop_back();
484         la->set_tooltip_text(tooltip);
485 
486         bu = Gtk::manage(new Gtk::Button);
487         bu->set_image_from_icon_name("window-close-symbolic", Gtk::ICON_SIZE_BUTTON);
488         bu->set_relief(Gtk::RELIEF_NONE);
489         bu->get_style_context()->add_class("tag-entry-tiny-button");
490         bu->get_style_context()->add_class("dim-label");
491         bu->show();
492         bu->signal_clicked().connect([this] {
493             parent->remove_filter(column.name);
494             parent->search();
495         });
496         pack_start(*bu, false, false, 0);
497     }
498 
499 private:
500     PoolBrowserParametric *parent;
501     const PoolParametric::Column &column;
502     Gtk::Label *la = nullptr;
503     Gtk::Button *bu = nullptr;
504 };
505 
update_filters_applied()506 void PoolBrowserParametric::update_filters_applied()
507 {
508     {
509         auto chs = filters_applied_box->get_children();
510         for (auto ch : chs) {
511             delete ch;
512         }
513     }
514     for (const auto &it : filters_applied) {
515         const auto &col = columns.at(it.first);
516         auto l = Gtk::manage(new FilterAppliedLabel(this, col, it.second));
517         l->show();
518         filters_applied_box->pack_start(*l, false, false, 0);
519     }
520 }
521 
remove_filter(const std::string & col)522 void PoolBrowserParametric::remove_filter(const std::string &col)
523 {
524     filter_boxes.at(col)->reset();
525     filters_applied.erase(col);
526 }
527 
uuid_from_row(const Gtk::TreeModel::Row & row)528 UUID PoolBrowserParametric::uuid_from_row(const Gtk::TreeModel::Row &row)
529 {
530     return row[list_columns.uuid];
531 }
pool_item_source_from_row(const Gtk::TreeModel::Row & row)532 PoolBrowser::PoolItemSource PoolBrowserParametric::pool_item_source_from_row(const Gtk::TreeModel::Row &row)
533 {
534     return row[list_columns.source];
535 }
536 
add_copy_name_context_menu_item()537 void PoolBrowserParametric::add_copy_name_context_menu_item()
538 {
539     add_context_menu_item("Copy MPN", [this](const UUID &uu) {
540         auto part = pool.get_part(uu);
541         auto clip = Gtk::Clipboard::get();
542         clip->set_text(part->get_MPN());
543     });
544 }
545 
get_stock_info_column()546 Gtk::TreeModelColumn<std::shared_ptr<StockInfoRecord>> &PoolBrowserParametric::get_stock_info_column()
547 {
548     return list_columns.stock_info;
549 }
550 
set_similar_part_uuid(const UUID & uu)551 void PoolBrowserParametric::set_similar_part_uuid(const UUID &uu)
552 {
553     similar_part_uuid = uu;
554 }
555 
filter_similar(const UUID & uu,float tol)556 void PoolBrowserParametric::filter_similar(const UUID &uu, float tol)
557 {
558     auto part = pool.get_part(uu);
559     if (part->parametric.count("table") == 0)
560         return;
561     if (part->parametric.at("table") != table.name)
562         return;
563     for (const auto &col : table.columns) {
564         if (part->parametric.count(col.name)) {
565             if (col.type == PoolParametric::Column::Type::QUANTITY) {
566                 if (values_remaining.count(col.name)) {
567                     std::string x = part->parametric.at(col.name);
568                     auto target = string_to_double(x);
569                     auto lo = target * (1 - tol);
570                     auto hi = target * (1 + tol);
571                     filters_applied[col.name].clear();
572                     for (const auto &it : values_remaining.at(col.name)) {
573                         auto v = string_to_double(it);
574                         if (v >= lo && v <= hi)
575                             filters_applied[col.name].insert(it);
576                     }
577                 }
578             }
579             else {
580                 filters_applied[col.name] = {part->parametric.at(col.name)};
581             }
582         }
583     }
584 }
585 
586 } // namespace horizon
587