1 #include "fab_output_window.hpp"
2 #include "board/board.hpp"
3 #include "export_gerber/gerber_export.hpp"
4 #include "util/gtk_util.hpp"
5 #include "widgets/spin_button_dim.hpp"
6 #include "document/idocument_board.hpp"
7 #include "rules/rules_with_core.hpp"
8 #include "rules/cache.hpp"
9 
10 namespace horizon {
11 
12 class GerberLayerEditor : public Gtk::Box, public Changeable {
13 public:
14     GerberLayerEditor(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &x, FabOutputWindow &pa,
15                       FabOutputSettings::GerberLayer &la);
16     static GerberLayerEditor *create(FabOutputWindow &pa, FabOutputSettings::GerberLayer &la);
17     FabOutputWindow &parent;
18 
19 private:
20     Gtk::CheckButton *gerber_layer_checkbutton = nullptr;
21     Gtk::Entry *gerber_layer_filename_entry = nullptr;
22 
23     FabOutputSettings::GerberLayer &layer;
24 };
25 
GerberLayerEditor(BaseObjectType * cobject,const Glib::RefPtr<Gtk::Builder> & x,FabOutputWindow & pa,FabOutputSettings::GerberLayer & la)26 GerberLayerEditor::GerberLayerEditor(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &x, FabOutputWindow &pa,
27                                      FabOutputSettings::GerberLayer &la)
28     : Gtk::Box(cobject), parent(pa), layer(la)
29 {
30     GET_WIDGET(gerber_layer_checkbutton);
31     GET_WIDGET(gerber_layer_filename_entry);
32     parent.sg_layer_name->add_widget(*gerber_layer_checkbutton);
33 
34     gerber_layer_checkbutton->set_label(parent.brd.get_layers().at(layer.layer).name);
35     bind_widget(gerber_layer_checkbutton, layer.enabled);
36     gerber_layer_checkbutton->signal_toggled().connect([this] { s_signal_changed.emit(); });
37     bind_widget(gerber_layer_filename_entry, layer.filename);
38     gerber_layer_filename_entry->signal_changed().connect([this] { s_signal_changed.emit(); });
39 }
40 
create(FabOutputWindow & pa,FabOutputSettings::GerberLayer & la)41 GerberLayerEditor *GerberLayerEditor::create(FabOutputWindow &pa, FabOutputSettings::GerberLayer &la)
42 {
43     GerberLayerEditor *w;
44     Glib::RefPtr<Gtk::Builder> x = Gtk::Builder::create();
45     x->add_from_resource("/org/horizon-eda/horizon/imp/fab_output.ui", "gerber_layer_editor");
46     x->get_widget_derived("gerber_layer_editor", w, pa, la);
47     w->reference();
48     return w;
49 }
50 
FabOutputWindow(BaseObjectType * cobject,const Glib::RefPtr<Gtk::Builder> & x,IDocumentBoard & c,const std::string & project_dir)51 FabOutputWindow::FabOutputWindow(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &x, IDocumentBoard &c,
52                                  const std::string &project_dir)
53     : Gtk::Window(cobject), core(c), brd(*core.get_board()), settings(core.get_fab_output_settings()),
54       state_store(this, "imp-fab-output")
55 {
56     GET_WIDGET(gerber_layers_box);
57     GET_WIDGET(prefix_entry);
58     GET_WIDGET(directory_entry);
59     GET_WIDGET(npth_filename_entry);
60     GET_WIDGET(pth_filename_entry);
61     GET_WIDGET(npth_filename_label);
62     GET_WIDGET(pth_filename_label);
63     GET_WIDGET(generate_button);
64     GET_WIDGET(directory_button);
65     GET_WIDGET(drill_mode_combo);
66     GET_WIDGET(log_textview);
67     GET_WIDGET(zip_output_switch);
68 
69     export_filechooser.attach(directory_entry, directory_button, this);
70     export_filechooser.set_project_dir(project_dir);
71     export_filechooser.bind_filename(settings.output_directory);
72     export_filechooser.signal_changed().connect([this] {
73         s_signal_changed.emit();
74         update_export_button();
75     });
76     export_filechooser.set_action(GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
77 
78     bind_widget(prefix_entry, settings.prefix, [this](std::string &) {
79         s_signal_changed.emit();
80         update_export_button();
81     });
82     prefix_entry->signal_changed().connect([this] { s_signal_changed.emit(); });
83     bind_widget(npth_filename_entry, settings.drill_npth_filename, [this](std::string &) {
84         s_signal_changed.emit();
85         update_export_button();
86     });
87     bind_widget(pth_filename_entry, settings.drill_pth_filename, [this](std::string &) {
88         s_signal_changed.emit();
89         update_export_button();
90     });
91     bind_widget(zip_output_switch, settings.zip_output);
92     zip_output_switch->property_active().signal_changed().connect([this] { s_signal_changed.emit(); });
93 
94     drill_mode_combo->set_active_id(FabOutputSettings::mode_lut.lookup_reverse(settings.drill_mode));
95     drill_mode_combo->signal_changed().connect([this] {
96         settings.drill_mode = FabOutputSettings::mode_lut.lookup(drill_mode_combo->get_active_id());
97         update_drill_visibility();
98         s_signal_changed.emit();
99         update_export_button();
100     });
101     update_drill_visibility();
102 
103     generate_button->signal_clicked().connect(sigc::mem_fun(*this, &FabOutputWindow::generate));
104 
105     outline_width_sp = Gtk::manage(new SpinButtonDim());
106     outline_width_sp->set_range(.01_mm, 10_mm);
107     outline_width_sp->show();
108     bind_widget(outline_width_sp, settings.outline_width);
109     outline_width_sp->signal_value_changed().connect([this] { s_signal_changed.emit(); });
110     {
111         Gtk::Box *b = nullptr;
112         x->get_widget("gerber_outline_width_box", b);
113         b->pack_start(*outline_width_sp, true, true, 0);
114     }
115 
116     sg_layer_name = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
117 
118     reload_layers();
119     update_export_button();
120 }
121 
reload_layers()122 void FabOutputWindow::reload_layers()
123 {
124     if (n_layers == settings.layers.size())
125         return;
126     else
127         n_layers = settings.layers.size();
128 
129     {
130         auto children = gerber_layers_box->get_children();
131         for (auto ch : children)
132             delete ch;
133     }
134     std::vector<FabOutputSettings::GerberLayer *> layers_sorted;
135     layers_sorted.reserve(settings.layers.size());
136     for (auto &la : settings.layers) {
137         layers_sorted.push_back(&la.second);
138     }
139     std::sort(layers_sorted.begin(), layers_sorted.end(),
140               [](const auto a, const auto b) { return b->layer < a->layer; });
141 
142     for (auto la : layers_sorted) {
143         auto ed = GerberLayerEditor::create(*this, *la);
144         ed->signal_changed().connect([this] {
145             s_signal_changed.emit();
146             update_export_button();
147         });
148         gerber_layers_box->add(*ed);
149         ed->show();
150         ed->unreference();
151     }
152 }
153 
update_drill_visibility()154 void FabOutputWindow::update_drill_visibility()
155 {
156     if (settings.drill_mode == FabOutputSettings::DrillMode::INDIVIDUAL) {
157         npth_filename_entry->set_visible(true);
158         npth_filename_label->set_visible(true);
159         pth_filename_label->set_text("PTH suffix");
160     }
161     else {
162         npth_filename_entry->set_visible(false);
163         npth_filename_label->set_visible(false);
164         pth_filename_label->set_text("Drill suffix");
165     }
166 }
167 
cb_nop(const std::string &)168 static void cb_nop(const std::string &)
169 {
170 }
171 
generate()172 void FabOutputWindow::generate()
173 {
174     if (!generate_button->get_sensitive())
175         return;
176 
177     RulesCheckCache cache(&core);
178     const auto r = rules_check(*core.get_rules(), RuleID::PREFLIGHT_CHECKS, core, cache, &cb_nop);
179     const auto r_plane = rules_check(*core.get_rules(), RuleID::PLANE, core, cache, &cb_nop);
180     if (r.level != RulesCheckErrorLevel::PASS || r_plane.level != RulesCheckErrorLevel::PASS) {
181         Gtk::MessageDialog md(*this, "Preflight or plane checks didn't pass", false /* use_markup */,
182                               Gtk::MESSAGE_ERROR, Gtk::BUTTONS_NONE);
183         md.set_secondary_text("This might be due to unfilled planes or overlapping planes of identical priority.");
184         md.add_button("Ignore", Gtk::RESPONSE_ACCEPT);
185         md.add_button("Cancel", Gtk::RESPONSE_CANCEL);
186         md.set_default_response(Gtk::RESPONSE_CANCEL);
187         if (md.run() != Gtk::RESPONSE_ACCEPT) {
188             return;
189         }
190     }
191 
192     try {
193         FabOutputSettings my_settings = settings;
194         my_settings.output_directory = export_filechooser.get_filename_abs();
195         my_settings.zip_output = zip_output_switch->get_active();
196         GerberExporter ex(brd, my_settings);
197         ex.generate();
198         log_textview->get_buffer()->set_text(ex.get_log());
199     }
200     catch (const std::exception &e) {
201         log_textview->get_buffer()->set_text(std::string("Error: ") + e.what());
202     }
203     catch (const Gio::Error &e) {
204         log_textview->get_buffer()->set_text(std::string("Error: ") + e.what());
205     }
206     catch (...) {
207         log_textview->get_buffer()->set_text("Other error");
208     }
209 }
210 
set_can_generate(bool v)211 void FabOutputWindow::set_can_generate(bool v)
212 {
213     can_export = v;
214     update_export_button();
215 }
216 
update_export_button()217 void FabOutputWindow::update_export_button()
218 {
219     std::string txt;
220     if (can_export) {
221         if (settings.output_directory.size()) {
222             if (settings.prefix.size()) {
223                 if (settings.drill_mode == FabOutputSettings::DrillMode::INDIVIDUAL) {
224                     if (settings.drill_pth_filename.size() == 0 || settings.drill_npth_filename.size() == 0) {
225                         txt = "drill filenames not set";
226                     }
227                 }
228                 else {
229                     if (settings.drill_pth_filename.size() == 0) {
230                         txt = "drill filename not set";
231                     }
232                 }
233                 if (txt.size() == 0) { // still okay
234                     for (const auto &it : settings.layers) {
235                         if (it.second.enabled && it.second.filename.size() == 0) {
236                             txt = brd.get_layers().at(it.first).name + " filename not set";
237                             break;
238                         }
239                     }
240                 }
241             }
242             else {
243                 txt = "prefix not set";
244             }
245         }
246         else {
247             txt = "output directory not set";
248         }
249     }
250     else {
251         txt = "tool is active";
252     }
253     widget_set_insensitive_tooltip(*generate_button, txt);
254 }
255 
create(Gtk::Window * p,IDocumentBoard & c,const std::string & project_dir)256 FabOutputWindow *FabOutputWindow::create(Gtk::Window *p, IDocumentBoard &c, const std::string &project_dir)
257 {
258     FabOutputWindow *w;
259     Glib::RefPtr<Gtk::Builder> x = Gtk::Builder::create();
260     x->add_from_resource("/org/horizon-eda/horizon/imp/fab_output.ui");
261     x->get_widget_derived("window", w, c, project_dir);
262 
263     w->set_transient_for(*p);
264 
265     return w;
266 }
267 } // namespace horizon
268