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