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