1 #include "bom_export_window.hpp"
2 #include "block/block.hpp"
3 #include "util/gtk_util.hpp"
4 #include "util/util.hpp"
5 #include "export_bom/export_bom.hpp"
6 #include "pool/part.hpp"
7 #include "pool/ipool.hpp"
8 #include "widgets/generic_combo_box.hpp"
9 #include "widgets/pool_browser_parametric.hpp"
10 #include "preferences/preferences_provider.hpp"
11 #include "preferences/preferences.hpp"
12 #include "util/stock_info_provider.hpp"
13 
14 namespace horizon {
15 
prepare_chooser(Glib::RefPtr<Gtk::FileChooser> chooser)16 void BOMExportWindow::MyExportFileChooser::prepare_chooser(Glib::RefPtr<Gtk::FileChooser> chooser)
17 {
18     auto filter = Gtk::FileFilter::create();
19     filter->set_name("CSV files");
20     filter->add_pattern("*.csv");
21     filter->add_pattern("*.CSV");
22     chooser->add_filter(filter);
23 }
24 
prepare_filename(std::string & filename)25 void BOMExportWindow::MyExportFileChooser::prepare_filename(std::string &filename)
26 {
27     if (!endswith(filename, ".csv") && !endswith(filename, ".CSV")) {
28         filename += ".csv";
29     }
30 }
31 
get_column_names() const32 std::map<int, std::string> BOMExportWindow::MyAdapter::get_column_names() const
33 {
34     std::map<int, std::string> r;
35     for (const auto &it : bom_column_names) {
36         r.emplace(static_cast<int>(it.first), it.second);
37     }
38     return r;
39 }
40 
get_column_name(int col) const41 std::string BOMExportWindow::MyAdapter::get_column_name(int col) const
42 {
43     return bom_column_names.at(static_cast<BOMColumn>(col));
44 }
45 
BOMExportWindow(BaseObjectType * cobject,const Glib::RefPtr<Gtk::Builder> & x,Block & blo,BOMExportSettings & s,IPool & p,const std::string & project_dir)46 BOMExportWindow::BOMExportWindow(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &x, Block &blo,
47                                  BOMExportSettings &s, IPool &p, const std::string &project_dir)
48     : Gtk::Window(cobject), block(blo), settings(s), pool(p), pool_parametric(pool.get_base_path()),
49       state_store(this, "imp-bom-export"), adapter(settings.csv_settings.columns)
50 {
51 
52     GET_WIDGET(export_button);
53     GET_WIDGET(nopopulate_check);
54     GET_WIDGET(filename_button);
55     GET_WIDGET(filename_entry);
56     GET_WIDGET(sort_column_combo);
57     GET_WIDGET(sort_order_combo);
58     GET_WIDGET(done_label);
59     GET_WIDGET(done_revealer);
60     GET_WIDGET(preview_tv);
61     GET_WIDGET(orderable_MPNs_listbox);
62     GET_OBJECT(sg_manufacturer);
63     GET_OBJECT(sg_MPN);
64     GET_OBJECT(sg_orderable_MPN);
65     GET_WIDGET(meta_parts_tv);
66     GET_WIDGET(param_browser_box);
67     GET_WIDGET(concrete_parts_label);
68     GET_WIDGET(rb_tol_10);
69     GET_WIDGET(rb_tol_1);
70     GET_WIDGET(button_clear_similar);
71     GET_WIDGET(button_set_similar);
72 
73     export_filechooser.attach(filename_entry, filename_button, this);
74     export_filechooser.set_project_dir(project_dir);
75     export_filechooser.bind_filename(settings.output_filename);
76     export_filechooser.signal_changed().connect([this] {
77         s_signal_changed.emit();
78         update_export_button();
79     });
80 
81     nopopulate_check->set_active(settings.include_nopopulate);
82     nopopulate_check->signal_toggled().connect([this] {
83         settings.include_nopopulate = nopopulate_check->get_active();
84         s_signal_changed.emit();
85         update_preview();
86     });
87 
88     for (const auto &it : bom_column_names) {
89         sort_column_combo->append(std::to_string(static_cast<int>(it.first)), it.second);
90     }
91     sort_column_combo->set_active_id(std::to_string(static_cast<int>(settings.csv_settings.sort_column)));
92     sort_column_combo->signal_changed().connect([this] {
93         settings.csv_settings.sort_column = static_cast<BOMColumn>(std::stoi(sort_column_combo->get_active_id()));
94         update_preview();
95         s_signal_changed.emit();
96     });
97 
98     {
99         const std::map<BOMExportSettings::CSVSettings::Order, std::string> its = {
100                 {BOMExportSettings::CSVSettings::Order::ASC, "Asc."},
101                 {BOMExportSettings::CSVSettings::Order::DESC, "Desc."},
102         };
103 
104         bind_widget<BOMExportSettings::CSVSettings::Order>(sort_order_combo, its, settings.csv_settings.order,
105                                                            [this](BOMExportSettings::CSVSettings::Order o) {
106                                                                update_preview();
107                                                                s_signal_changed.emit();
108                                                            });
109     }
110 
111     {
112         Gtk::Box *column_chooser_box;
113         GET_WIDGET(column_chooser_box);
114         column_chooser = ColumnChooser::create(adapter);
115         column_chooser_box->pack_start(*column_chooser, true, true, 0);
116         column_chooser->show();
117         column_chooser->unreference();
118         column_chooser->signal_changed().connect([this] {
119             s_signal_changed.emit();
120             update_preview();
121         });
122     }
123 
124     bom_store = Gtk::ListStore::create(list_columns_preview);
125     preview_tv->set_model(bom_store);
126 
127     meta_parts_store = Gtk::ListStore::create(meta_parts_list_columns);
128     meta_parts_tv->set_model(meta_parts_store);
129     meta_parts_tv->append_column("QTY", meta_parts_list_columns.qty);
130     meta_parts_tv->append_column("MPN", meta_parts_list_columns.MPN);
131     meta_parts_tv->append_column("Value", meta_parts_list_columns.value);
132     meta_parts_tv->append_column("Manufacturer", meta_parts_list_columns.manufacturer);
133     meta_parts_tv->append_column("S MPN", meta_parts_list_columns.concrete_MPN);
134     meta_parts_tv->append_column("S Value", meta_parts_list_columns.concrete_value);
135     meta_parts_tv->append_column("S Manufacturer", meta_parts_list_columns.concrete_manufacturer);
136 
137     meta_parts_tv->get_selection()->signal_changed().connect(
138             sigc::mem_fun(*this, &BOMExportWindow::update_concrete_parts));
139     rb_tol_10->signal_toggled().connect(sigc::mem_fun(*this, &BOMExportWindow::update_concrete_parts));
140     rb_tol_1->set_active(true);
141 
142     button_set_similar->signal_clicked().connect(sigc::mem_fun(*this, &BOMExportWindow::handle_set_similar));
143 
144     button_clear_similar->signal_clicked().connect([this] {
145         settings.concrete_parts.erase(meta_part_current);
146         update_meta_mapping();
147         s_signal_changed.emit();
148         update_similar_button_sensitivity();
149         update_preview();
150         update_orderable_MPNs();
151     });
152     update_similar_button_sensitivity();
153 
154     export_button->signal_clicked().connect(sigc::mem_fun(*this, &BOMExportWindow::generate));
155 
156     update();
157     update_export_button();
158 }
159 
update_similar_button_sensitivity()160 void BOMExportWindow::update_similar_button_sensitivity()
161 {
162     if (!meta_part_current) {
163         button_set_similar->set_sensitive(false);
164         button_clear_similar->set_sensitive(false);
165         return;
166     }
167     button_clear_similar->set_sensitive(settings.concrete_parts.count(meta_part_current));
168     button_set_similar->set_sensitive(browser_param ? browser_param->get_selected() : false);
169 }
170 
handle_set_similar()171 void BOMExportWindow::handle_set_similar()
172 {
173     auto sel = browser_param->get_selected();
174     if (sel) {
175         settings.concrete_parts[meta_part_current] = pool.get_part(sel);
176         update_meta_mapping();
177         s_signal_changed.emit();
178     }
179     update_similar_button_sensitivity();
180     update_preview();
181     update_orderable_MPNs();
182 }
183 
flash(const std::string & s)184 void BOMExportWindow::flash(const std::string &s)
185 {
186     done_label->set_text(s);
187     done_revealer->set_reveal_child(true);
188     flash_connection.disconnect();
189     flash_connection = Glib::signal_timeout().connect(
190             [this] {
191                 done_revealer->set_reveal_child(false);
192                 return false;
193             },
194             2000);
195 }
196 
generate()197 void BOMExportWindow::generate()
198 {
199     try {
200         export_BOM(export_filechooser.get_filename_abs(), block, settings);
201         flash("BOM written to " + settings.output_filename);
202     }
203     catch (const std::exception &e) {
204         flash("Error: " + std::string(e.what()));
205     }
206     catch (...) {
207         flash("unknown error");
208     }
209     update_preview();
210 }
211 
update_preview()212 void BOMExportWindow::update_preview()
213 {
214     auto rows = block.get_BOM(settings);
215     std::vector<BOMRow> bom;
216     std::transform(rows.begin(), rows.end(), std::back_inserter(bom), [](const auto &it) { return it.second; });
217     std::sort(bom.begin(), bom.end(), [this](const auto &a, const auto &b) {
218         auto sa = a.get_column(settings.csv_settings.sort_column);
219         auto sb = b.get_column(settings.csv_settings.sort_column);
220         auto c = strcmp_natural(sa, sb);
221         if (settings.csv_settings.order == BOMExportSettings::CSVSettings::Order::ASC)
222             return c < 0;
223         else
224             return c > 0;
225     });
226 
227     preview_tv->remove_all_columns();
228     bom_store->clear();
229     for (const auto &it : bom) {
230         Gtk::TreeModel::Row row = *bom_store->append();
231         row[list_columns_preview.row] = it;
232     }
233 
234     for (auto col : settings.csv_settings.columns) {
235         auto cr_text = Gtk::manage(new Gtk::CellRendererText());
236         auto tvc = Gtk::manage(new Gtk::TreeViewColumn(bom_column_names.at(col)));
237         tvc->set_resizable();
238         tvc->pack_start(*cr_text, true);
239         if (col == BOMColumn::REFDES) {
240             cr_text->property_ellipsize() = Pango::ELLIPSIZE_END;
241             tvc->set_expand(true);
242         }
243         else if (col == BOMColumn::DATASHEET) {
244             cr_text->property_ellipsize() = Pango::ELLIPSIZE_END;
245         }
246         tvc->set_cell_data_func(*cr_text, [this, col](Gtk::CellRenderer *tcr, const Gtk::TreeModel::iterator &it) {
247             Gtk::TreeModel::Row row = *it;
248             BOMRow bomrow = row[list_columns_preview.row];
249             auto mcr = dynamic_cast<Gtk::CellRendererText *>(tcr);
250             mcr->property_text() = bomrow.get_column(col);
251         });
252         preview_tv->append_column(*tvc);
253     }
254 }
255 
256 class OrderableMPNSelector : public Gtk::Box {
257 public:
OrderableMPNSelector(const Part & p,BOMExportWindow & par)258     OrderableMPNSelector(const Part &p, BOMExportWindow &par)
259         : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 10), part(p), parent(par), settings(parent.settings)
260     {
261         property_margin() = 5;
262         set_margin_start(10);
263         set_margin_end(10);
264         auto la_manufacturer = Gtk::manage(new Gtk::Label(part.get_manufacturer()));
265         la_manufacturer->set_xalign(0);
266         la_manufacturer->show();
267         pack_start(*la_manufacturer, false, false, 0);
268         parent.sg_manufacturer->add_widget(*la_manufacturer);
269 
270         auto la_MPN = Gtk::manage(new Gtk::Label(part.get_MPN()));
271         la_MPN->set_xalign(0);
272         la_MPN->show();
273         pack_start(*la_MPN, false, false, 0);
274         parent.sg_MPN->add_widget(*la_MPN);
275 
276         auto combo = Gtk::manage(new GenericComboBox<UUID>());
277         combo->append(UUID(), part.get_MPN() + " (base)");
278         std::vector<std::pair<UUID, std::string>> orderable_MPNs_sorted;
279 
280         for (const auto &it : part.orderable_MPNs) {
281             orderable_MPNs_sorted.push_back(it);
282         }
283         std::sort(orderable_MPNs_sorted.begin(), orderable_MPNs_sorted.end(),
284                   [](const auto &a, const auto &b) { return strcmp_natural(a.second, b.second) < 0; });
285         for (const auto &it : orderable_MPNs_sorted) {
286             combo->append(it.first, it.second);
287         }
288 
289         combo->show();
290         pack_start(*combo, false, false, 0);
291         parent.sg_orderable_MPN->add_widget(*combo);
292 
293         if (settings.orderable_MPNs.count(part.uuid))
294             combo->set_active_key(settings.orderable_MPNs.at(part.uuid));
295         else
296             combo->set_active_key(UUID());
297         combo->signal_changed().connect([this, combo] {
298             settings.orderable_MPNs[part.uuid] = combo->get_active_key();
299             parent.update_preview();
300             parent.signal_changed().emit();
301         });
302     }
303 
304 private:
305     const Part &part;
306     BOMExportWindow &parent;
307     BOMExportSettings &settings;
308 };
309 
update_orderable_MPNs()310 void BOMExportWindow::update_orderable_MPNs()
311 {
312     {
313         auto children = orderable_MPNs_listbox->get_children();
314         for (auto ch : children) {
315             delete ch;
316         }
317     }
318     BOMExportSettings my_settings(settings);
319     my_settings.include_nopopulate = true;
320     auto rows = block.get_BOM(my_settings);
321     for (const auto &row : rows) {
322         auto part = row.first;
323         if (part->orderable_MPNs.size()) {
324             auto ed = Gtk::manage(new OrderableMPNSelector(*part, *this));
325             ed->show();
326             orderable_MPNs_listbox->append(*ed);
327         }
328     }
329 }
330 
update()331 void BOMExportWindow::update()
332 {
333     update_preview();
334     update_orderable_MPNs();
335     meta_parts_tv->unset_model();
336     meta_parts_store->clear();
337     std::map<const Part *, unsigned int> parts;
338     for (const auto &it : block.components) {
339         if (it.second.part) {
340             parts[it.second.part]++;
341         }
342     }
343     for (const auto &it : parts) {
344         if (it.first->parametric.count("table")) {
345             Gtk::TreeModel::Row row = *meta_parts_store->append();
346             row[meta_parts_list_columns.MPN] = it.first->get_MPN();
347             row[meta_parts_list_columns.value] = it.first->get_value();
348             row[meta_parts_list_columns.manufacturer] = it.first->get_manufacturer();
349             row[meta_parts_list_columns.uuid] = it.first->uuid;
350             row[meta_parts_list_columns.qty] = it.second;
351         }
352     }
353     meta_parts_tv->set_model(meta_parts_store);
354     update_meta_mapping();
355     update_concrete_parts();
356 }
357 
update_meta_mapping()358 void BOMExportWindow::update_meta_mapping()
359 {
360     for (auto it : meta_parts_store->children()) {
361         Gtk::TreeModel::Row row = *it;
362         UUID uu = row[meta_parts_list_columns.uuid];
363         if (settings.concrete_parts.count(uu)) {
364             auto part = settings.concrete_parts.at(uu);
365             row[meta_parts_list_columns.concrete_MPN] = part->get_MPN();
366             row[meta_parts_list_columns.concrete_value] = part->get_value();
367             row[meta_parts_list_columns.concrete_manufacturer] = part->get_manufacturer();
368         }
369         else {
370             row[meta_parts_list_columns.concrete_MPN] = "";
371             row[meta_parts_list_columns.concrete_value] = "";
372             row[meta_parts_list_columns.concrete_manufacturer] = "";
373         }
374     }
375 }
376 
update_concrete_parts()377 void BOMExportWindow::update_concrete_parts()
378 {
379     auto sel = meta_parts_tv->get_selection()->get_selected();
380     if (browser_param) {
381         delete browser_param;
382         browser_param = nullptr;
383     }
384     if (sel) {
385         auto row = *sel;
386         auto part = pool.get_part(row[meta_parts_list_columns.uuid]);
387         meta_part_current = part->uuid;
388         concrete_parts_label->set_text("Similar parts to " + part->get_MPN());
389 
390         browser_param = Gtk::manage(new PoolBrowserParametric(pool, pool_parametric, part->parametric.at("table")));
391         browser_param->set_similar_part_uuid(part->uuid);
392         browser_param->search();
393         float tol = rb_tol_10->get_active() ? .1 : .01;
394         browser_param->filter_similar(part->uuid, tol);
395         if (auto prv = StockInfoProvider::create(pool.get_base_path())) {
396             browser_param->add_stock_info_provider(std::move(prv));
397         }
398         browser_param->search();
399         param_browser_box->pack_start(*browser_param, true, true);
400         browser_param->show();
401         browser_param->signal_activated().connect(sigc::mem_fun(*this, &BOMExportWindow::handle_set_similar));
402         browser_param->signal_selected().connect(
403                 sigc::mem_fun(*this, &BOMExportWindow::update_similar_button_sensitivity));
404         update_similar_button_sensitivity();
405     }
406     else {
407         concrete_parts_label->set_text("Select part to pick similar part");
408         meta_part_current = UUID();
409     }
410 }
411 
set_can_export(bool v)412 void BOMExportWindow::set_can_export(bool v)
413 {
414     can_export = v;
415     update_export_button();
416 }
417 
update_export_button()418 void BOMExportWindow::update_export_button()
419 {
420     std::string txt;
421     if (can_export) {
422         if (settings.output_filename.size() == 0) {
423             txt = "output filename not set";
424         }
425     }
426     else {
427         txt = "tool is active";
428     }
429     widget_set_insensitive_tooltip(*export_button, txt);
430 }
431 
create(Gtk::Window * p,Block & b,BOMExportSettings & s,IPool & pool,const std::string & project_dir)432 BOMExportWindow *BOMExportWindow::create(Gtk::Window *p, Block &b, BOMExportSettings &s, IPool &pool,
433                                          const std::string &project_dir)
434 {
435     BOMExportWindow *w;
436     Glib::RefPtr<Gtk::Builder> x = Gtk::Builder::create();
437     x->add_from_resource("/org/horizon-eda/horizon/imp/bom_export.ui");
438     x->get_widget_derived("window", w, b, s, pool, project_dir);
439 
440     w->set_transient_for(*p);
441 
442     return w;
443 }
444 } // namespace horizon
445