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 ∂
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