1 #include "preferences_window_canvas.hpp"
2 #include "common/lut.hpp"
3 #include "common/common.hpp"
4 #include "canvas/color_palette.hpp"
5 #include "canvas/appearance.hpp"
6 #include "preferences/preferences.hpp"
7 #include "board/board_layers.hpp"
8 #include "util/gtk_util.hpp"
9 #include "util/util.hpp"
10 #include "nlohmann/json.hpp"
11 #include <set>
12 #include <iostream>
13 #include <iomanip>
14 
15 namespace horizon {
16 
17 static const std::map<ColorP, std::string> color_names = {
18         {ColorP::JUNCTION, "Junction"},
19         {ColorP::FRAG_ORPHAN, "Orphan fragment"},
20         {ColorP::AIRWIRE_ROUTER, "Router airwire"},
21         {ColorP::TEXT_OVERLAY, "Text overlay"},
22         {ColorP::HOLE, "Hole"},
23         {ColorP::DIMENSION, "Dimension"},
24         {ColorP::ERROR, "Error"},
25         {ColorP::NET, "Net"},
26         {ColorP::BUS, "Bus"},
27         {ColorP::FRAME, "Frame"},
28         {ColorP::AIRWIRE, "Airwire"},
29         {ColorP::PIN, "Pin"},
30         {ColorP::PIN_HIDDEN, "Hidden Pin"},
31         {ColorP::DIFFPAIR, "Diff. pair"},
32         {ColorP::NOPOPULATE_X, "Do not pop. X-out"},
33         {ColorP::PROJECTION, "3D projection"},
34         {ColorP::BACKGROUND, "Background"},
35         {ColorP::GRID, "Grid"},
36         {ColorP::CURSOR_NORMAL, "Cursor"},
37         {ColorP::CURSOR_TARGET, "Cursor on target"},
38         {ColorP::ORIGIN, "Origin"},
39         {ColorP::MARKER_BORDER, "Marker border"},
40         {ColorP::SELECTION_BOX, "Selection box"},
41         {ColorP::SELECTION_LINE, "Selection line"},
42         {ColorP::SELECTABLE_OUTER, "Selectable outer"},
43         {ColorP::SELECTABLE_INNER, "Selectable inner"},
44         {ColorP::SELECTABLE_PRELIGHT, "Selectable prelight"},
45         {ColorP::SELECTABLE_ALWAYS, "Selectable always"},
46         {ColorP::SEARCH, "Search markers"},
47         {ColorP::SEARCH_CURRENT, "Current search marker"},
48         {ColorP::SHADOW, "Highlight shadow"},
49 };
50 
51 static const std::set<ColorP> colors_non_layer = {ColorP::NET,         ColorP::BUS,        ColorP::FRAME,
52                                                   ColorP::PIN,         ColorP::PIN_HIDDEN, ColorP::DIFFPAIR,
53                                                   ColorP::NOPOPULATE_X};
54 
55 static const std::set<ColorP> colors_layer = {ColorP::FRAG_ORPHAN, ColorP::AIRWIRE_ROUTER, ColorP::TEXT_OVERLAY,
56                                               ColorP::HOLE,        ColorP::DIMENSION,      ColorP::AIRWIRE,
57                                               ColorP::SHADOW,      ColorP::PROJECTION};
58 
59 class ColorEditor : public Gtk::Box {
60 public:
61     ColorEditor();
62     void construct();
63     virtual Color get_color() = 0;
64     virtual void set_color(const Color &c) = 0;
65 
66 protected:
67     virtual std::string get_color_name() = 0;
68     Gtk::DrawingArea *colorbox = nullptr;
69 };
70 
ColorEditor()71 ColorEditor::ColorEditor() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 5)
72 {
73 }
74 
construct()75 void ColorEditor::construct()
76 {
77     auto la = Gtk::manage(new Gtk::Label(get_color_name()));
78     la->set_xalign(0);
79     Gdk::RGBA rgba;
80     auto co = get_color();
81     rgba.set_red(co.r);
82     rgba.set_green(co.g);
83     rgba.set_blue(co.b);
84     rgba.set_alpha(1);
85     colorbox = Gtk::manage(new Gtk::DrawingArea);
86     colorbox->set_size_request(20, -1);
87     colorbox->show();
88     colorbox->signal_draw().connect([this](const Cairo::RefPtr<Cairo::Context> &cr) -> bool {
89         auto c = get_color();
90         cr->set_source_rgb(c.r, c.g, c.b);
91         cr->paint();
92         return true;
93     });
94     pack_start(*colorbox, false, false, 0);
95     pack_start(*la, false, false, 0);
96     la->show();
97     colorbox->show();
98     set_margin_start(5);
99     set_margin_end(5);
100 }
101 
102 class ColorEditorPalette : public ColorEditor {
103 public:
104     ColorEditorPalette(Appearance &a, Preferences &p, ColorP c);
105 
106 private:
107     Appearance &apperance;
108     Preferences &prefs;
109     ColorP color;
110 
111     Color get_color() override;
112     void set_color(const Color &c) override;
113     std::string get_color_name() override;
114 };
115 
ColorEditorPalette(Appearance & a,Preferences & p,ColorP c)116 ColorEditorPalette::ColorEditorPalette(Appearance &a, Preferences &p, ColorP c) : apperance(a), prefs(p), color(c)
117 {
118 }
119 
get_color()120 Color ColorEditorPalette::get_color()
121 {
122     return apperance.colors.at(color);
123 }
124 
set_color(const Color & c)125 void ColorEditorPalette::set_color(const Color &c)
126 {
127     apperance.colors.at(color) = c;
128     queue_draw();
129     prefs.signal_changed().emit();
130 }
131 
get_color_name()132 std::string ColorEditorPalette::get_color_name()
133 {
134     return color_names.at(color);
135 }
136 
137 class ColorEditorLayer : public ColorEditor {
138 public:
139     ColorEditorLayer(Appearance &a, Preferences &p, int layer, const std::string &na = "");
140 
141 private:
142     Appearance &apperance;
143     Preferences &prefs;
144     int layer;
145     std::string name;
146 
147     Color get_color() override;
148     void set_color(const Color &c) override;
149     std::string get_color_name() override;
150 };
151 
ColorEditorLayer(Appearance & a,Preferences & p,int l,const std::string & na)152 ColorEditorLayer::ColorEditorLayer(Appearance &a, Preferences &p, int l, const std::string &na)
153     : apperance(a), prefs(p), layer(l), name(na)
154 {
155 }
156 
get_color()157 Color ColorEditorLayer::get_color()
158 {
159     return apperance.layer_colors.at(layer);
160 }
161 
set_color(const Color & c)162 void ColorEditorLayer::set_color(const Color &c)
163 {
164     apperance.layer_colors.at(layer) = c;
165     queue_draw();
166     prefs.signal_changed().emit();
167 }
168 
get_color_name()169 std::string ColorEditorLayer::get_color_name()
170 {
171     if (name.size())
172         return name;
173     else
174         return BoardLayers::get_layer_name(layer);
175 }
176 
177 static const std::vector<std::pair<std::string, Appearance::CursorSize>> cursor_size_labels = {
178         {"Default", Appearance::CursorSize::DEFAULT},
179         {"Large", Appearance::CursorSize::LARGE},
180         {"Full", Appearance::CursorSize::FULL},
181 };
182 
make_cursor_size_editor(Gtk::Box * box,Appearance::CursorSize & cursor_size,std::function<void (void)> cb)183 static void make_cursor_size_editor(Gtk::Box *box, Appearance::CursorSize &cursor_size, std::function<void(void)> cb)
184 {
185     std::map<Appearance::CursorSize, Gtk::RadioButton *> widgets;
186 
187     box->get_style_context()->add_class("linked");
188     Gtk::RadioButton *last_rb = nullptr;
189     for (const auto &it : cursor_size_labels) {
190         auto rb = Gtk::manage(new Gtk::RadioButton(it.first));
191         widgets.emplace(it.second, rb);
192         rb->set_mode(false);
193         rb->show();
194         if (last_rb)
195             rb->join_group(*last_rb);
196         box->pack_start(*rb, true, true, 0);
197         last_rb = rb;
198     }
199     bind_widget<Appearance::CursorSize>(widgets, cursor_size, [cb](auto _) { cb(); });
200 }
201 
spinbutton_set_px(Gtk::SpinButton & sp)202 static void spinbutton_set_px(Gtk::SpinButton &sp)
203 {
204     sp.signal_output().connect([&sp] {
205         auto v = sp.get_value();
206         std::ostringstream oss;
207         oss.imbue(get_locale());
208         oss << std::fixed << std::setprecision(1) << v << " px";
209         sp.set_text(oss.str());
210         return true;
211     });
212     entry_set_tnum(sp);
213 }
214 
CanvasPreferencesEditor(BaseObjectType * cobject,const Glib::RefPtr<Gtk::Builder> & x,Preferences & prefs,bool layered)215 CanvasPreferencesEditor::CanvasPreferencesEditor(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &x,
216                                                  Preferences &prefs, bool layered)
217     : Gtk::Box(cobject), preferences(prefs),
218       canvas_preferences(layered ? preferences.canvas_layer : preferences.canvas_non_layer), is_layered(layered)
219 {
220 
221     Gtk::RadioButton *canvas_grid_style_cross, *canvas_grid_style_dot, *canvas_grid_style_grid,
222             *canvas_grid_fine_mod_alt, *canvas_grid_fine_mod_ctrl;
223     GET_WIDGET(canvas_grid_style_cross);
224     GET_WIDGET(canvas_grid_style_dot);
225     GET_WIDGET(canvas_grid_style_grid);
226     GET_WIDGET(canvas_grid_style_grid);
227     GET_WIDGET(canvas_grid_fine_mod_alt);
228     GET_WIDGET(canvas_grid_fine_mod_ctrl);
229 
230     Gtk::Scale *canvas_grid_opacity, *canvas_highlight_dim, *canvas_highlight_lighten;
231     GET_WIDGET(canvas_grid_opacity);
232     GET_WIDGET(canvas_highlight_dim);
233     GET_WIDGET(canvas_highlight_lighten);
234 
235     Gtk::Label *canvas_label;
236     GET_WIDGET(canvas_label);
237 
238     GET_WIDGET(canvas_colors_fb);
239 
240     Gtk::Menu *color_preset_menu;
241     GET_WIDGET(color_preset_menu);
242 
243     color_presets = json_from_resource("/org/horizon-eda/horizon/preferences/color_presets.json");
244 
245     {
246 
247         {
248             auto it = Gtk::manage(new Gtk::MenuItem("Export…"));
249             it->signal_activate().connect(sigc::mem_fun(*this, &CanvasPreferencesEditor::handle_export));
250             it->show();
251             color_preset_menu->append(*it);
252         }
253         {
254             auto it = Gtk::manage(new Gtk::MenuItem("Import…"));
255             it->signal_activate().connect(sigc::mem_fun(*this, &CanvasPreferencesEditor::handle_import));
256             it->show();
257             color_preset_menu->append(*it);
258         }
259         {
260             auto it = Gtk::manage(new Gtk::MenuItem("Default"));
261             it->signal_activate().connect(sigc::mem_fun(*this, &CanvasPreferencesEditor::handle_default));
262             it->show();
263             color_preset_menu->append(*it);
264         }
265         unsigned int idx = 0;
266         for (auto it = color_presets.cbegin(); it != color_presets.cend(); ++it) {
267             const auto &v = it.value();
268             if (v.value("layered", false) == is_layered) {
269                 auto item = Gtk::manage(new Gtk::MenuItem(v.at("name").get<std::string>()));
270                 item->signal_activate().connect([this, idx] { handle_load_preset(idx); });
271                 item->show();
272                 color_preset_menu->append(*item);
273             }
274             idx++;
275         }
276     }
277 
278     color_chooser = Glib::wrap(GTK_COLOR_CHOOSER(gtk_builder_get_object(x->gobj(), "canvas_color_chooser")), true);
279     color_chooser_conn = color_chooser->property_rgba().signal_changed().connect([this] {
280         auto sel = canvas_colors_fb->get_selected_children();
281         if (sel.size() != 1)
282             return;
283         auto it = dynamic_cast<ColorEditor *>(sel.front()->get_child());
284         Color c;
285         auto rgba = color_chooser->get_rgba();
286         c.r = rgba.get_red();
287         c.g = rgba.get_green();
288         c.b = rgba.get_blue();
289         it->set_color(c);
290     });
291 
292     canvas_colors_fb->signal_selected_children_changed().connect(
293             sigc::mem_fun(*this, &CanvasPreferencesEditor::update_color_chooser));
294 
295     if (is_layered) {
296         canvas_label->set_text("Affects Padstack, Package and Board");
297     }
298     else {
299         canvas_label->set_text("Affects Symbol and Schematic");
300     }
301 
302     std::map<Appearance::GridStyle, Gtk::RadioButton *> grid_style_widgets = {
303             {Appearance::GridStyle::CROSS, canvas_grid_style_cross},
304             {Appearance::GridStyle::DOT, canvas_grid_style_dot},
305             {Appearance::GridStyle::GRID, canvas_grid_style_grid},
306     };
307 
308     std::map<Appearance::GridFineModifier, Gtk::RadioButton *> grid_fine_mod_widgets = {
309             {Appearance::GridFineModifier::ALT, canvas_grid_fine_mod_alt},
310             {Appearance::GridFineModifier::CTRL, canvas_grid_fine_mod_ctrl},
311     };
312 
313     auto &appearance = canvas_preferences.appearance;
314 
315     bind_widget(grid_style_widgets, appearance.grid_style);
316     bind_widget(grid_fine_mod_widgets, appearance.grid_fine_modifier);
317     bind_widget(canvas_grid_opacity, appearance.grid_opacity);
318     bind_widget(canvas_highlight_dim, appearance.highlight_dim);
319     bind_widget(canvas_highlight_lighten, appearance.highlight_lighten);
320 
321     for (auto &it : grid_style_widgets) {
322         it.second->signal_toggled().connect([this] { preferences.signal_changed().emit(); });
323     }
324     for (auto &it : grid_fine_mod_widgets) {
325         it.second->signal_toggled().connect([this] { preferences.signal_changed().emit(); });
326     }
327     canvas_grid_opacity->signal_value_changed().connect([this] { preferences.signal_changed().emit(); });
328     canvas_highlight_dim->signal_value_changed().connect([this] { preferences.signal_changed().emit(); });
329     canvas_highlight_lighten->signal_value_changed().connect([this] { preferences.signal_changed().emit(); });
330 
331     Gtk::Box *cursor_size_tool_box, *cursor_size_box;
332     GET_WIDGET(cursor_size_box);
333     GET_WIDGET(cursor_size_tool_box);
334     make_cursor_size_editor(cursor_size_box, appearance.cursor_size, [this] { preferences.signal_changed().emit(); });
335     make_cursor_size_editor(cursor_size_tool_box, appearance.cursor_size_tool,
336                             [this] { preferences.signal_changed().emit(); });
337 
338     {
339         Gtk::SpinButton *min_line_width_sp;
340         GET_WIDGET(min_line_width_sp);
341         spinbutton_set_px(*min_line_width_sp);
342         min_line_width_sp->set_value(appearance.min_line_width);
343         min_line_width_sp->signal_value_changed().connect([this, min_line_width_sp, &appearance] {
344             appearance.min_line_width = min_line_width_sp->get_value();
345             preferences.signal_changed().emit();
346         });
347     }
348     {
349         Gtk::SpinButton *min_selectable_size_sp;
350         GET_WIDGET(min_selectable_size_sp);
351         spinbutton_set_px(*min_selectable_size_sp);
352         min_selectable_size_sp->set_value(appearance.min_selectable_size);
353         min_selectable_size_sp->signal_value_changed().connect([this, min_selectable_size_sp, &appearance] {
354             appearance.min_selectable_size = min_selectable_size_sp->get_value();
355             preferences.signal_changed().emit();
356         });
357     }
358     {
359         Gtk::SpinButton *snap_radius_sp;
360         GET_WIDGET(snap_radius_sp);
361         spinbutton_set_px(*snap_radius_sp);
362         snap_radius_sp->set_value(appearance.snap_radius);
363         snap_radius_sp->signal_value_changed().connect([this, snap_radius_sp, &appearance] {
364             appearance.snap_radius = snap_radius_sp->get_value();
365             preferences.signal_changed().emit();
366         });
367     }
368 
369     {
370         Gtk::ComboBoxText *msaa_combo;
371         GET_WIDGET(msaa_combo);
372         msaa_combo->append("0", "Off");
373         for (int i = 1; i < 5; i *= 2) {
374             msaa_combo->append(std::to_string(i), std::to_string(i) + "× MSAA");
375         }
376         msaa_combo->set_active_id(std::to_string(appearance.msaa));
377         msaa_combo->signal_changed().connect([this, msaa_combo, &appearance] {
378             int msaa = std::stoi(msaa_combo->get_active_id());
379             appearance.msaa = msaa;
380             preferences.signal_changed().emit();
381         });
382     }
383 
384     std::vector<ColorEditor *> ws;
385     for (const auto &it : color_names) {
386         bool add = !colors_layer.count(it.first) && !colors_non_layer.count(it.first); // in both
387         if (layered) {
388             add = add || colors_layer.count(it.first);
389         }
390         else {
391             add = add || colors_non_layer.count(it.first);
392         }
393         if (add)
394             ws.push_back(Gtk::manage(new ColorEditorPalette(appearance, prefs, it.first)));
395     }
396 
397     if (layered) {
398         for (const auto la : BoardLayers::get_layers()) {
399             ws.push_back(Gtk::manage(new ColorEditorLayer(appearance, prefs, la)));
400         }
401     }
402     else {
403         ws.push_back(Gtk::manage(new ColorEditorLayer(appearance, prefs, 0, "Default layer")));
404     }
405     ws.push_back(Gtk::manage(new ColorEditorLayer(appearance, prefs, 10000, "Non-layer")));
406     for (auto w : ws) {
407         w->construct();
408         w->show();
409         canvas_colors_fb->add(*w);
410     }
411     canvas_colors_fb->select_child(*canvas_colors_fb->get_child_at_index(0));
412 }
413 
handle_export()414 void CanvasPreferencesEditor::handle_export()
415 {
416     auto top = dynamic_cast<Gtk::Window *>(get_ancestor(GTK_TYPE_WINDOW));
417     std::string filename;
418     {
419 
420         GtkFileChooserNative *native = gtk_file_chooser_native_new("Save color scheme", GTK_WINDOW(top->gobj()),
421                                                                    GTK_FILE_CHOOSER_ACTION_SAVE, "_Save", "_Cancel");
422         auto chooser = Glib::wrap(GTK_FILE_CHOOSER(native));
423         chooser->set_do_overwrite_confirmation(true);
424         chooser->set_current_name("colors.json");
425 
426         if (gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)) == GTK_RESPONSE_ACCEPT) {
427             filename = chooser->get_filename();
428             if (!endswith(filename, ".json"))
429                 filename += ".json";
430         }
431     }
432     if (filename.size()) {
433         while (1) {
434             std::string error_str;
435             try {
436                 auto j = canvas_preferences.serialize_colors();
437                 j["layered"] = is_layered;
438                 save_json_to_file(filename, j);
439                 break;
440             }
441             catch (const std::exception &e) {
442                 error_str = e.what();
443             }
444             catch (...) {
445                 error_str = "unknown error";
446             }
447             if (error_str.size()) {
448                 Gtk::MessageDialog dia(*top, "Export error", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_NONE);
449                 dia.set_secondary_text(error_str);
450                 dia.add_button("Cancel", Gtk::RESPONSE_CANCEL);
451                 dia.add_button("Retry", 1);
452                 if (dia.run() != 1)
453                     break;
454             }
455         }
456     }
457 }
458 
handle_import()459 void CanvasPreferencesEditor::handle_import()
460 {
461     auto top = dynamic_cast<Gtk::Window *>(get_ancestor(GTK_TYPE_WINDOW));
462     GtkFileChooserNative *native = gtk_file_chooser_native_new("Open color scheme", GTK_WINDOW(top->gobj()),
463                                                                GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
464     auto chooser = Glib::wrap(GTK_FILE_CHOOSER(native));
465     auto filter = Gtk::FileFilter::create();
466     filter->set_name("Horizon color schemes");
467     filter->add_pattern("*.json");
468     chooser->add_filter(filter);
469 
470     if (gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)) == GTK_RESPONSE_ACCEPT) {
471         auto filename = chooser->get_filename();
472         std::string error_str;
473         try {
474             auto j = load_json_from_file(filename);
475             if (j.value("layered", false) != is_layered) {
476                 if (is_layered)
477                     throw std::runtime_error("Can't load non-layer file into layer settings");
478                 else
479                     throw std::runtime_error("Can't load layer file into non-layer settings");
480             }
481             load_colors(j);
482         }
483         catch (const std::exception &e) {
484             error_str = e.what();
485         }
486         catch (...) {
487             error_str = "unknown error";
488         }
489         if (error_str.size()) {
490             Gtk::MessageDialog dia(*top, "Import error", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK);
491             dia.set_secondary_text(error_str);
492             dia.run();
493         }
494     }
495 }
496 
handle_load_preset(unsigned int idx)497 void CanvasPreferencesEditor::handle_load_preset(unsigned int idx)
498 {
499     load_colors(color_presets.at(idx));
500 }
501 
handle_default()502 void CanvasPreferencesEditor::handle_default()
503 {
504     CanvasPreferences def;
505     if (!is_layered)
506         def.appearance.layer_colors[0] = {1, 1, 0};
507     canvas_preferences.appearance.colors = def.appearance.colors;
508     canvas_preferences.appearance.layer_colors = def.appearance.layer_colors;
509     preferences.signal_changed().emit();
510     update_color_chooser();
511     queue_draw();
512     canvas_colors_fb->queue_draw();
513 }
514 
load_colors(const json & j)515 void CanvasPreferencesEditor::load_colors(const json &j)
516 {
517     canvas_preferences.load_colors_from_json(j);
518     preferences.signal_changed().emit();
519     update_color_chooser();
520     queue_draw();
521     canvas_colors_fb->queue_draw();
522 }
523 
update_color_chooser()524 void CanvasPreferencesEditor::update_color_chooser()
525 {
526     auto sel = canvas_colors_fb->get_selected_children();
527     if (sel.size() != 1)
528         return;
529     auto it = dynamic_cast<ColorEditor *>(sel.front()->get_child());
530     color_chooser_conn.block();
531     Gdk::RGBA rgba;
532     color_chooser->set_rgba(rgba); // fixes things...
533     auto c = it->get_color();
534     rgba.set_rgba(c.r, c.g, c.b, 1);
535     color_chooser->set_rgba(rgba);
536     color_chooser_conn.unblock();
537 }
538 
create(Preferences & prefs,bool layered)539 CanvasPreferencesEditor *CanvasPreferencesEditor::create(Preferences &prefs, bool layered)
540 {
541     CanvasPreferencesEditor *w;
542     Glib::RefPtr<Gtk::Builder> x = Gtk::Builder::create();
543     std::vector<Glib::ustring> widgets = {"canvas_box",  "adjustment1", "adjustment2",
544                                           "adjustment3", "adjustment4", "adjustment7",
545                                           "adjustment8", "adjustment9", "color_preset_menu"};
546     x->add_from_resource("/org/horizon-eda/horizon/pool-prj-mgr/preferences/preferences.ui", widgets);
547     x->get_widget_derived("canvas_box", w, prefs, layered);
548     w->reference();
549     return w;
550 }
551 } // namespace horizon
552