1 #include "preferences.hpp"
2 #include "common/lut.hpp"
3 #include "util/util.hpp"
4 #include <fstream>
5 #include <giomm/file.h>
6 #include <glibmm/fileutils.h>
7 #include <glibmm/miscutils.h>
8 #include "nlohmann/json.hpp"
9 #include "logger/logger.hpp"
10 #include "imp/in_tool_action_catalog.hpp"
11 #include "core/tool_id.hpp"
12 #include "imp/actions.hpp"
13 
14 namespace horizon {
15 
16 static const LutEnumStr<Appearance::GridStyle> grid_style_lut = {
17         {"cross", Appearance::GridStyle::CROSS},
18         {"dot", Appearance::GridStyle::DOT},
19         {"grid", Appearance::GridStyle::GRID},
20 };
21 
22 static const LutEnumStr<Appearance::GridFineModifier> grid_fine_mod_lut = {
23         {"alt", Appearance::GridFineModifier::ALT},
24         {"ctrl", Appearance::GridFineModifier::CTRL},
25 };
26 
27 static const LutEnumStr<Appearance::CursorSize> cursor_size_lut = {
28         {"default", Appearance::CursorSize::DEFAULT},
29         {"large", Appearance::CursorSize::LARGE},
30         {"full", Appearance::CursorSize::FULL},
31 };
32 
33 static const LutEnumStr<Preferences::StockInfoProviderSel> stock_info_provider_lut = {
34         {"none", Preferences::StockInfoProviderSel::NONE},
35         {"partinfo", Preferences::StockInfoProviderSel::PARTINFO},
36         {"digikey", Preferences::StockInfoProviderSel::DIGIKEY}};
37 
38 #ifdef G_OS_WIN32
39 static const bool capture_output_default = true;
40 #else
41 static const bool capture_output_default = false;
42 #endif
43 
Preferences()44 Preferences::Preferences()
45 {
46     canvas_non_layer.appearance.layer_colors[0] = {1, 1, 0};
47     capture_output = capture_output_default;
48 }
49 
load_default()50 void Preferences::load_default()
51 {
52     canvas_layer = CanvasPreferences();
53     canvas_non_layer = CanvasPreferences();
54     canvas_non_layer.appearance.layer_colors[0] = {1, 1, 0};
55     key_sequences.load_from_json(json_from_resource("/org/horizon-eda/horizon/imp/keys_default.json"));
56     in_tool_key_sequences.load_from_json(json_from_resource("/org/horizon-eda/horizon/imp/in_tool_keys_default.json"));
57     capture_output = capture_output_default;
58 }
59 
get_preferences_filename()60 std::string Preferences::get_preferences_filename()
61 {
62     return Glib::build_filename(get_config_dir(), "prefs.json");
63 }
64 
65 #define COLORP_LUT_ITEM(x)                                                                                             \
66     {                                                                                                                  \
67 #x, ColorP::x                                                                                                  \
68     }
69 
70 static const LutEnumStr<ColorP> colorp_lut = {
71         COLORP_LUT_ITEM(FROM_LAYER),
72         COLORP_LUT_ITEM(JUNCTION),
73         COLORP_LUT_ITEM(FRAG_ORPHAN),
74         COLORP_LUT_ITEM(AIRWIRE_ROUTER),
75         COLORP_LUT_ITEM(TEXT_OVERLAY),
76         COLORP_LUT_ITEM(HOLE),
77         COLORP_LUT_ITEM(DIMENSION),
78         COLORP_LUT_ITEM(ERROR),
79         COLORP_LUT_ITEM(NET),
80         COLORP_LUT_ITEM(BUS),
81         COLORP_LUT_ITEM(FRAME),
82         COLORP_LUT_ITEM(AIRWIRE),
83         COLORP_LUT_ITEM(PIN),
84         COLORP_LUT_ITEM(PIN_HIDDEN),
85         COLORP_LUT_ITEM(DIFFPAIR),
86         COLORP_LUT_ITEM(BACKGROUND),
87         COLORP_LUT_ITEM(GRID),
88         COLORP_LUT_ITEM(CURSOR_NORMAL),
89         COLORP_LUT_ITEM(CURSOR_TARGET),
90         COLORP_LUT_ITEM(ORIGIN),
91         COLORP_LUT_ITEM(MARKER_BORDER),
92         COLORP_LUT_ITEM(SELECTION_BOX),
93         COLORP_LUT_ITEM(SELECTION_LINE),
94         COLORP_LUT_ITEM(SELECTABLE_OUTER),
95         COLORP_LUT_ITEM(SELECTABLE_INNER),
96         COLORP_LUT_ITEM(SELECTABLE_PRELIGHT),
97         COLORP_LUT_ITEM(SELECTABLE_ALWAYS),
98         COLORP_LUT_ITEM(SEARCH),
99         COLORP_LUT_ITEM(SEARCH_CURRENT),
100         COLORP_LUT_ITEM(SHADOW),
101         COLORP_LUT_ITEM(CONNECTION_LINE),
102         COLORP_LUT_ITEM(NOPOPULATE_X),
103         COLORP_LUT_ITEM(PROJECTION),
104 };
105 
serialize() const106 json CanvasPreferences::serialize() const
107 {
108     json j = serialize_colors();
109     j["grid_style"] = grid_style_lut.lookup_reverse(appearance.grid_style);
110     j["grid_opacity"] = appearance.grid_opacity;
111     j["highlight_dim"] = appearance.highlight_dim;
112     j["highlight_lighten"] = appearance.highlight_lighten;
113     j["grid_fine_modifier"] = grid_fine_mod_lut.lookup_reverse(appearance.grid_fine_modifier);
114     j["cursor_size"] = cursor_size_lut.lookup_reverse(appearance.cursor_size);
115     j["cursor_size_tool"] = cursor_size_lut.lookup_reverse(appearance.cursor_size_tool);
116     j["msaa"] = appearance.msaa;
117     j["min_line_width"] = appearance.min_line_width;
118     j["min_selectable_size"] = appearance.min_selectable_size;
119     j["snap_radius"] = appearance.snap_radius;
120     return j;
121 }
122 
serialize_colors() const123 json CanvasPreferences::serialize_colors() const
124 {
125     json j;
126     json j_layer_colors = json::object();
127     for (const auto &it : appearance.layer_colors) {
128         j_layer_colors[std::to_string(it.first)] = color_to_json(it.second);
129     }
130     j["layer_colors"] = j_layer_colors;
131 
132     json j_colors = json::object();
133     for (const auto &it : appearance.colors) {
134         j_colors[colorp_lut.lookup_reverse(it.first)] = color_to_json(it.second);
135     }
136     j["colors"] = j_colors;
137     return j;
138 }
139 
load_colors_from_json(const json & j)140 void CanvasPreferences::load_colors_from_json(const json &j)
141 {
142     if (j.count("layer_colors")) {
143         const auto &o = j.at("layer_colors");
144         for (auto it = o.cbegin(); it != o.cend(); ++it) {
145             int l = std::stoi(it.key());
146             appearance.layer_colors[l] = color_from_json(it.value());
147         }
148     }
149     if (j.count("colors")) {
150         const auto &o = j.at("colors");
151         for (auto it = o.cbegin(); it != o.cend(); ++it) {
152             auto c = colorp_lut.lookup(it.key(), ColorP::FROM_LAYER);
153             if (c != ColorP::FROM_LAYER)
154                 appearance.colors[c] = color_from_json(it.value());
155         }
156     }
157 }
158 
load_from_json(const json & j)159 void CanvasPreferences::load_from_json(const json &j)
160 {
161     appearance.grid_style = grid_style_lut.lookup(j.at("grid_style"));
162     appearance.grid_opacity = j.value("grid_opacity", .4);
163     appearance.highlight_dim = j.value("highlight_dim", .3);
164     appearance.highlight_lighten = j.value("highlight_lighten", .3);
165     appearance.grid_fine_modifier = grid_fine_mod_lut.lookup(j.value("grid_fine_modifier", "alt"));
166     appearance.cursor_size = cursor_size_lut.lookup(j.value("cursor_size", "default"));
167     appearance.cursor_size_tool = cursor_size_lut.lookup(j.value("cursor_size_tool", "default"));
168     appearance.msaa = j.value("msaa", 0);
169     appearance.min_line_width = j.value("min_line_width", 1.0);
170     appearance.min_selectable_size = j.value("min_selectable_size", 20.0);
171     appearance.snap_radius = j.value("snap_radius", 30.0);
172     load_colors_from_json(j);
173 }
174 
serialize() const175 json SchematicPreferences::serialize() const
176 {
177     json j;
178     j["show_all_junctions"] = show_all_junctions;
179     j["drag_start_net_line"] = drag_start_net_line;
180     j["bend_non_ortho"] = bend_non_ortho;
181     return j;
182 }
183 
load_from_json(const json & j)184 void SchematicPreferences::load_from_json(const json &j)
185 {
186     show_all_junctions = j.value("show_all_junctions", false);
187     drag_start_net_line = j.value("drag_start_net_line", true);
188     bend_non_ortho = j.value("bend_non_ortho", true);
189 }
190 
serialize() const191 json BoardPreferences::serialize() const
192 {
193     json j;
194     j["drag_start_track"] = drag_start_track;
195     j["highlight_on_top"] = highlight_on_top;
196     j["show_text_in_tracks"] = show_text_in_tracks;
197     j["show_text_in_vias"] = show_text_in_vias;
198     j["move_using_router"] = move_using_router;
199     return j;
200 }
201 
load_from_json(const json & j)202 void BoardPreferences::load_from_json(const json &j)
203 {
204     drag_start_track = j.value("drag_start_track", true);
205     highlight_on_top = j.value("highlight_on_top", true);
206     show_text_in_tracks = j.value("show_text_in_tracks", true);
207     show_text_in_vias = j.value("show_text_in_vias", true);
208     move_using_router = j.value("move_using_router", true);
209 }
210 
serialize() const211 json ZoomPreferences::serialize() const
212 {
213     json j;
214     j["smooth_zoom_2d"] = smooth_zoom_2d;
215     j["smooth_zoom_3d"] = smooth_zoom_3d;
216     j["touchpad_pan"] = touchpad_pan;
217     j["zoom_factor"] = zoom_factor;
218     j["keyboard_zoom_to_cursor"] = keyboard_zoom_to_cursor;
219     return j;
220 }
221 
load_from_json(const json & j)222 void ZoomPreferences::load_from_json(const json &j)
223 {
224     smooth_zoom_2d = j.value("smooth_zoom_2d", true);
225     smooth_zoom_3d = j.value("smooth_zoom_3d", false);
226     touchpad_pan = j.value("touchpad_pan", false);
227     zoom_factor = j.value("zoom_factor", 50);
228     keyboard_zoom_to_cursor = j.value("keyboard_zoom_to_cursor", false);
229 }
230 
serialize() const231 json KeySequencesPreferences::serialize() const
232 {
233     json j;
234     for (const auto &it : keys) {
235         json k;
236         auto a_str = action_lut.lookup_reverse(it.first.first);
237         auto t_str = tool_lut.lookup_reverse(it.first.second);
238         k["action"] = a_str;
239         k["tool"] = t_str;
240         k["keys"] = json::object();
241         for (const auto &it2 : it.second) {
242             k["keys"][std::to_string(static_cast<int>(it2.first))] = json::array();
243             for (const auto &it3 : it2.second) {
244                 json seq;
245                 for (const auto &it4 : it3) {
246                     json o;
247                     o["key"] = gdk_keyval_name(it4.first);
248                     o["mod"] = static_cast<int>(it4.second);
249                     seq.push_back(o);
250                 }
251                 if (seq.size())
252                     k["keys"][std::to_string(static_cast<int>(it2.first))].push_back(seq);
253             }
254         }
255         j.push_back(k);
256     }
257     return j;
258 }
259 
load_from_json(const json & j)260 void KeySequencesPreferences::load_from_json(const json &j)
261 {
262     keys.clear();
263     append_from_json(j);
264 }
265 
append_from_json(const json & j)266 void KeySequencesPreferences::append_from_json(const json &j)
267 {
268     for (const auto &it : j) {
269         try {
270             auto action = action_lut.lookup(it.at("action"), ActionID::NONE);
271             if (action != ActionID::NONE) {
272                 auto tool = tool_lut.lookup(it.at("tool"), ToolID::NONE);
273                 if (action != ActionID::TOOL || (action == ActionID::TOOL && tool != ToolID::NONE)) {
274                     auto k = std::make_pair(action, tool);
275                     if (keys.count(k) == 0) {
276                         keys[k];
277                         auto &j2 = it.at("keys");
278                         for (auto it2 = j2.cbegin(); it2 != j2.cend(); ++it2) {
279                             auto av = static_cast<ActionCatalogItem::Availability>(std::stoi(it2.key()));
280                             keys[k][av];
281                             for (const auto &it3 : it2.value()) {
282                                 keys[k][av].emplace_back();
283                                 for (const auto &it4 : it3) {
284                                     std::string keyname = it4.at("key");
285                                     auto key = gdk_keyval_from_name(keyname.c_str());
286                                     auto mod = static_cast<GdkModifierType>(it4.at("mod").get<int>());
287                                     keys[k][av].back().emplace_back(key, mod);
288                                 }
289                             }
290                         }
291                     }
292                 }
293             }
294         }
295         catch (const std::exception &e) {
296             Logger::log_warning("error loading key sequence", Logger::Domain::UNSPECIFIED, e.what());
297         }
298         catch (...) {
299             Logger::log_warning("error loading key sequence", Logger::Domain::UNSPECIFIED, "unknown error");
300         }
301     }
302 }
serialize() const303 json InToolKeySequencesPreferences::serialize() const
304 {
305     json j;
306     for (const auto &[action, sequences] : keys) {
307         auto a_str = in_tool_action_lut.lookup_reverse(action);
308         for (const auto &it2 : sequences) {
309             json seq;
310             for (const auto &it4 : it2) {
311                 json o;
312                 o["key"] = gdk_keyval_name(it4.first);
313                 o["mod"] = static_cast<int>(it4.second);
314                 seq.push_back(o);
315             }
316             if (seq.size())
317                 j[a_str].push_back(seq);
318         }
319     }
320     return j;
321 }
322 
load_from_json(const json & j)323 void InToolKeySequencesPreferences::load_from_json(const json &j)
324 {
325     keys.clear();
326     append_from_json(j);
327 }
328 
append_from_json(const json & j)329 void InToolKeySequencesPreferences::append_from_json(const json &j)
330 {
331     for (const auto &[a_str, keys_seqs] : j.items()) {
332         try {
333             auto action = in_tool_action_lut.lookup(a_str, InToolActionID::NONE);
334             if (action != InToolActionID::NONE) {
335                 if (keys.count(action) == 0) {
336                     for (const auto &seq : keys_seqs) {
337                         keys[action].emplace_back();
338                         for (const auto &it4 : seq) {
339                             std::string keyname = it4.at("key");
340                             auto key = gdk_keyval_from_name(keyname.c_str());
341                             auto mod = static_cast<GdkModifierType>(it4.at("mod").get<int>());
342                             keys[action].back().emplace_back(key, mod);
343                         }
344                     }
345                 }
346             }
347         }
348         catch (const std::exception &e) {
349             Logger::log_warning("error loading in-tool key sequence", Logger::Domain::UNSPECIFIED, e.what());
350         }
351         catch (...) {
352             Logger::log_warning("error loading int-tool key sequence", Logger::Domain::UNSPECIFIED, "unknown error");
353         }
354     }
355 }
356 
load_from_json(const json & j)357 void PartInfoPreferences::load_from_json(const json &j)
358 {
359     url = j.at("url");
360     preferred_distributor = j.at("preferred_distributor");
361     ignore_moq_gt_1 = j.at("ignore_moq_gt_1");
362     max_price_breaks = j.value("max_price_breaks", 3);
363 }
364 
serialize() const365 json PartInfoPreferences::serialize() const
366 {
367     json j;
368     j["enable"] = false;
369     j["url"] = url;
370     j["preferred_distributor"] = preferred_distributor;
371     j["ignore_moq_gt_1"] = ignore_moq_gt_1;
372     j["max_price_breaks"] = max_price_breaks;
373     return j;
374 }
375 
load_from_json(const json & j)376 void DigiKeyApiPreferences::load_from_json(const json &j)
377 {
378     client_id = j.at("client_id");
379     client_secret = j.at("client_secret");
380     currency = j.value("currency", "EUR");
381     site = j.value("site", "DE");
382     max_price_breaks = j.value("max_price_breaks", 3);
383 }
384 
serialize() const385 json DigiKeyApiPreferences::serialize() const
386 {
387     json j;
388     j["enable"] = false;
389     j["client_id"] = client_id;
390     j["client_secret"] = client_secret;
391     j["site"] = site;
392     j["currency"] = currency;
393     j["max_price_breaks"] = max_price_breaks;
394     return j;
395 }
396 
serialize() const397 json ActionBarPreferences::serialize() const
398 {
399     json j;
400     j["enable"] = enable;
401     j["remember"] = remember;
402     j["show_in_tool"] = show_in_tool;
403     return j;
404 }
405 
load_from_json(const json & j)406 void ActionBarPreferences::load_from_json(const json &j)
407 {
408     enable = j.value("enable", true);
409     remember = j.value("remember", true);
410     show_in_tool = j.value("show_in_tool", true);
411 }
412 
serialize() const413 json MousePreferences::serialize() const
414 {
415     json j;
416     j["switch_layers"] = switch_layers;
417     j["switch_sheets"] = switch_sheets;
418     j["drag_polygon_edges"] = drag_polygon_edges;
419     j["drag_to_move"] = drag_to_move;
420     return j;
421 }
422 
load_from_json(const json & j)423 void MousePreferences::load_from_json(const json &j)
424 {
425     switch_layers = j.value("switch_layers", true);
426     switch_sheets = j.value("switch_sheets", true);
427     drag_polygon_edges = j.value("drag_polygon_edges", true);
428     drag_to_move = j.value("drag_to_move", true);
429 }
430 
serialize() const431 json Preferences::serialize() const
432 {
433     json j;
434     j["canvas_layer"] = canvas_layer.serialize();
435     j["canvas_non_layer"] = canvas_non_layer.serialize();
436     j["schematic"] = schematic.serialize();
437     j["key_sequences"] = key_sequences.serialize();
438     j["in_tool_key_sequences"] = in_tool_key_sequences.serialize();
439     j["board"] = board.serialize();
440     j["zoom"] = zoom.serialize();
441     j["capture_output"] = capture_output;
442     j["stock_info_provider"] = stock_info_provider_lut.lookup_reverse(stock_info_provider);
443     j["partinfo"] = partinfo.serialize();
444     j["digikey_api"] = digikey_api.serialize();
445     j["action_bar"] = action_bar.serialize();
446     j["mouse"] = mouse.serialize();
447     j["show_pull_request_tools"] = show_pull_request_tools;
448     j["hud_debug"] = hud_debug;
449     return j;
450 }
451 
save()452 void Preferences::save()
453 {
454     std::string prefs_filename = get_preferences_filename();
455 
456     json j = serialize();
457     save_json_to_file(prefs_filename, j);
458 }
459 
load_from_json(const json & j)460 void Preferences::load_from_json(const json &j)
461 {
462 
463     canvas_layer.load_from_json(j.at("canvas_layer"));
464     canvas_non_layer.load_from_json(j.at("canvas_non_layer"));
465     if (j.count("schematic"))
466         schematic.load_from_json(j.at("schematic"));
467     if (j.count("board"))
468         board.load_from_json(j.at("board"));
469     if (j.count("zoom"))
470         zoom.load_from_json(j.at("zoom"));
471     if (j.count("key_sequences"))
472         key_sequences.load_from_json(j.at("key_sequences"));
473     if (j.count("in_tool_key_sequences"))
474         in_tool_key_sequences.load_from_json(j.at("in_tool_key_sequences"));
475     if (j.count("action_bar"))
476         action_bar.load_from_json(j.at("action_bar"));
477     if (j.count("mouse"))
478         mouse.load_from_json(j.at("mouse"));
479     key_sequences.append_from_json(json_from_resource("/org/horizon-eda/horizon/imp/keys_default.json"));
480     in_tool_key_sequences.append_from_json(
481             json_from_resource("/org/horizon-eda/horizon/imp/in_tool_keys_default.json"));
482     capture_output = j.value("capture_output", capture_output_default);
483     show_pull_request_tools = j.value("show_pull_request_tools", false);
484     hud_debug = j.value("hud_debug", false);
485     stock_info_provider = stock_info_provider_lut.lookup(j.value("stock_info_provider", "none"));
486     if (j.count("partinfo"))
487         partinfo.load_from_json(j.at("partinfo"));
488     if (j.count("digikey_api"))
489         digikey_api.load_from_json(j.at("digikey_api"));
490 }
491 
load()492 void Preferences::load()
493 {
494     std::string prefs_filename = get_preferences_filename();
495     std::string prefs_filename_old = Glib::build_filename(get_config_dir(), "imp-prefs.json");
496     if (!Glib::file_test(prefs_filename, Glib::FILE_TEST_IS_REGULAR)) {
497         if (Glib::file_test(prefs_filename_old, Glib::FILE_TEST_IS_REGULAR)) {
498             json j = load_json_from_file(prefs_filename_old);
499             load_from_json(j);
500         }
501         else {
502             load_default();
503         }
504     }
505     else {
506         json j = load_json_from_file(prefs_filename);
507         load_from_json(j);
508     }
509     s_signal_changed.emit();
510 }
511 } // namespace horizon
512