1 #include "imp.hpp"
2 #include "block/block.hpp"
3 #include "canvas/canvas_gl.hpp"
4 #include "export_gerber/gerber_export.hpp"
5 #include "logger/logger.hpp"
6 #include "pool/part.hpp"
7 #include "pool/project_pool.hpp"
8 #include "property_panels/property_panels.hpp"
9 #include "rules/rules_window.hpp"
10 #include "util/gtk_util.hpp"
11 #include "util/util.hpp"
12 #include "util/geom_util.hpp"
13 #include "widgets/log_view.hpp"
14 #include "widgets/log_window.hpp"
15 #include "widgets/spin_button_dim.hpp"
16 #include "action_catalog.hpp"
17 #include "tool_popover.hpp"
18 #include "util/selection_util.hpp"
19 #include "util/str_util.hpp"
20 #include "preferences/preferences_provider.hpp"
21 #include "parameter_window.hpp"
22 #include "widgets/about_dialog.hpp"
23 #include <glibmm/main.h>
24 #include <glibmm/markup.h>
25 #include <gtkmm.h>
26 #include <iomanip>
27 #include <functional>
28 #include "nlohmann/json.hpp"
29 #include "core/tool_id.hpp"
30 #include "actions.hpp"
31 #include "preferences/preferences_util.hpp"
32 #include "widgets/action_button.hpp"
33 #include "in_tool_action_catalog.hpp"
34 #include "util/zmq_helper.hpp"
35 #include "pool/pool_parametric.hpp"
36 #include "action_icon.hpp"
37 #include "grids_window.hpp"
38
39 #ifdef G_OS_WIN32
40 #include <winsock2.h>
41 #endif
42 #include "util/win32_undef.hpp"
43
44 namespace horizon {
45
46 class PoolWithParametric : public ProjectPool {
47 public:
PoolWithParametric(const std::string & bp,bool cache)48 PoolWithParametric(const std::string &bp, bool cache) : ProjectPool(bp, cache), parametric(bp)
49 {
50 }
51
get_parametric()52 PoolParametric *get_parametric() override
53 {
54 return ¶metric;
55 }
56
57 private:
58 PoolParametric parametric;
59 };
60
make_pool(const PoolParams & p)61 static std::unique_ptr<Pool> make_pool(const PoolParams &p)
62 {
63 if (PoolInfo(p.base_path).uuid == PoolInfo::project_pool_uuid) {
64 return std::make_unique<PoolWithParametric>(p.base_path, false);
65 }
66 else {
67 return std::make_unique<Pool>(p.base_path);
68 }
69 }
70
71
make_pool_caching(const PoolParams & p)72 static std::unique_ptr<Pool> make_pool_caching(const PoolParams &p)
73 {
74 if (PoolInfo(p.base_path).uuid == PoolInfo::project_pool_uuid) {
75 return std::make_unique<PoolWithParametric>(p.base_path, true);
76 }
77 else {
78 return nullptr;
79 }
80 }
81
82
ImpBase(const PoolParams & params)83 ImpBase::ImpBase(const PoolParams ¶ms)
84 : pool(make_pool(params)), real_pool_caching(make_pool_caching(params)),
85 pool_caching(real_pool_caching ? real_pool_caching.get() : pool.get()), core(nullptr),
86 sock_broadcast_rx(zctx, ZMQ_SUB), sock_project(zctx, ZMQ_REQ), drag_tool(ToolID::NONE)
87 {
88 auto ep_broadcast = Glib::getenv("HORIZON_EP_BROADCAST");
89 if (ep_broadcast.size()) {
90 sock_broadcast_rx.connect(ep_broadcast);
91 zmq_helper::subscribe_int(sock_broadcast_rx, 0);
92 zmq_helper::subscribe_int(sock_broadcast_rx, getpid());
93 auto chan = zmq_helper::io_channel_from_socket(sock_broadcast_rx);
94
95 {
96 auto pid_p = Glib::getenv("HORIZON_MGR_PID");
97 if (pid_p.size())
98 mgr_pid = std::stoi(pid_p);
99 }
100
101 Glib::signal_io().connect(
102 [this](Glib::IOCondition cond) {
103 while (zmq_helper::can_recv(sock_broadcast_rx)) {
104 zmq::message_t msg;
105 if (zmq_helper::recv(sock_broadcast_rx, msg)) {
106 int prefix;
107 memcpy(&prefix, msg.data(), 4);
108 char *data = ((char *)msg.data()) + 4;
109 json j = json::parse(data);
110 if (prefix == 0 || prefix == getpid()) {
111 handle_broadcast(j);
112 }
113 }
114 }
115 return true;
116 },
117 chan, Glib::IO_IN | Glib::IO_HUP);
118 }
119 auto ep_project = Glib::getenv("HORIZON_EP_MGR");
120 if (ep_project.size()) {
121 sock_project.connect(ep_project);
122 zmq_helper::set_timeouts(sock_project, 5000);
123 }
124 sockets_connected = ep_project.size() && ep_broadcast.size();
125 }
126
127
show_sockets_broken_dialog(const std::string & msg)128 void ImpBase::show_sockets_broken_dialog(const std::string &msg)
129 {
130 Gtk::MessageDialog md(*main_window, "Lost connection to project/pool manager", false /* use_markup */,
131 Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK);
132 md.set_secondary_text("Save your work an reopen the current document\n" + msg);
133 md.run();
134 }
135
send_json(const json & j)136 json ImpBase::send_json(const json &j)
137 {
138 if (sockets_broken) {
139 show_sockets_broken_dialog();
140 return nullptr;
141 }
142 if (!sockets_connected)
143 return nullptr;
144
145 std::string s = j.dump();
146 zmq::message_t msg(s.size() + 1);
147 memcpy(((uint8_t *)msg.data()), s.c_str(), s.size());
148 auto m = (char *)msg.data();
149 m[msg.size() - 1] = 0;
150 try {
151 if (zmq_helper::send(sock_project, msg) == false) {
152 sockets_broken = true;
153 sockets_connected = false;
154 show_sockets_broken_dialog("send timeout");
155 return nullptr;
156 }
157 }
158 catch (zmq::error_t &e) {
159 sockets_broken = true;
160 sockets_connected = false;
161 show_sockets_broken_dialog(e.what());
162 return nullptr;
163 }
164
165 zmq::message_t rx;
166 try {
167 if (zmq_helper::recv(sock_project, rx) == false) {
168 sockets_broken = true;
169 sockets_connected = false;
170 show_sockets_broken_dialog("receive timeout");
171 return nullptr;
172 }
173 }
174 catch (zmq::error_t &e) {
175 sockets_broken = true;
176 sockets_connected = false;
177 show_sockets_broken_dialog(e.what());
178 return nullptr;
179 }
180 char *rxdata = ((char *)rx.data());
181 return json::parse(rxdata);
182 }
183
handle_close(const GdkEventAny * ev)184 bool ImpBase::handle_close(const GdkEventAny *ev)
185 {
186 bool dontask = false;
187 Glib::getenv("HORIZON_NOEXITCONFIRM", dontask);
188 if (dontask) {
189 core->delete_autosave();
190 return false;
191 }
192
193 if (!core->get_needs_save()) {
194 core->delete_autosave();
195 return false;
196 }
197 if (!read_only) {
198 Gtk::MessageDialog md(*main_window, "Save changes before closing?", false /* use_markup */,
199 Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE);
200 md.set_secondary_text("If you don't save, all your changes will be permanently lost.");
201 md.add_button("Close without Saving", 1);
202 md.add_button("Cancel", Gtk::RESPONSE_CANCEL);
203 md.add_button("Save", 2);
204 md.set_default_response(Gtk::RESPONSE_CANCEL);
205 switch (md.run()) {
206 case 1:
207 core->delete_autosave();
208 return false; // close
209
210 case 2:
211 force_end_tool();
212 trigger_action(ActionID::SAVE);
213 core->delete_autosave();
214 return false; // close
215
216 default:
217 return true; // keep window open
218 }
219 }
220 else {
221 Gtk::MessageDialog md(*main_window, "Document is read only", false /* use_markup */, Gtk::MESSAGE_QUESTION,
222 Gtk::BUTTONS_NONE);
223 md.set_secondary_text("You won't be able to save your changes");
224 md.add_button("Close without Saving", 1);
225 md.add_button("Cancel", Gtk::RESPONSE_CANCEL);
226 switch (md.run()) {
227 case 1:
228 core->delete_autosave();
229 return false; // close
230
231 default:
232 return true; // keep window open
233 }
234 }
235
236 core->delete_autosave();
237 return false;
238 }
239
240 #undef GET_WIDGET
241 #define GET_WIDGET(name) \
242 do { \
243 main_window->builder->get_widget(#name, name); \
244 } while (0)
245
update_selection_label()246 void ImpBase::update_selection_label()
247 {
248 std::string la = "Selection (";
249 switch (canvas->selection_tool) {
250 case CanvasGL::SelectionTool::BOX:
251 la += "Box";
252 break;
253
254 case CanvasGL::SelectionTool::LASSO:
255 la += "Lasso";
256 break;
257
258 case CanvasGL::SelectionTool::PAINT:
259 la += "Paint";
260 break;
261 }
262 la += ", ";
263 switch (canvas->selection_qualifier) {
264 case CanvasGL::SelectionQualifier::INCLUDE_BOX:
265 la += "include box";
266 break;
267 case CanvasGL::SelectionQualifier::INCLUDE_ORIGIN:
268 la += "include origin";
269 break;
270 case CanvasGL::SelectionQualifier::TOUCH_BOX:
271 la += "touch box";
272 break;
273 case CanvasGL::SelectionQualifier::AUTO:
274 la += "auto";
275 break;
276 }
277 la += ")";
278 main_window->selection_label->set_text(la);
279 }
280
get_tool_settings_filename(ToolID id)281 std::string ImpBase::get_tool_settings_filename(ToolID id)
282 {
283 auto type_str = object_type_lut.lookup_reverse(get_editor_type());
284 auto settings_dir = Glib::build_filename(get_config_dir(), "tool_settings", type_str);
285 if (!Glib::file_test(settings_dir, Glib::FILE_TEST_IS_DIR))
286 Gio::File::create_for_path(settings_dir)->make_directory_with_parents();
287 return Glib::build_filename(settings_dir, tool_lut.lookup_reverse(id) + ".json");
288 }
289
property_panel_has_focus()290 bool ImpBase::property_panel_has_focus()
291 {
292 auto focus_widget = main_window->get_focus();
293 bool property_has_focus = false;
294 if (focus_widget && (dynamic_cast<Gtk::Entry *>(focus_widget) || dynamic_cast<Gtk::TextView *>(focus_widget)))
295 property_has_focus = focus_widget->is_ancestor(*main_window->property_viewport);
296 return property_has_focus;
297 }
298
set_flip_view(bool flip)299 void ImpBase::set_flip_view(bool flip)
300 {
301 canvas->set_flip_view(flip);
302 canvas_update_from_pp();
303 update_view_hints();
304 g_simple_action_set_state(bottom_view_action->gobj(), g_variant_new_boolean(canvas->get_flip_view()));
305 }
306
run(int argc,char * argv[])307 void ImpBase::run(int argc, char *argv[])
308 {
309 auto app = Gtk::Application::create(argc, argv, "org.horizon_eda.HorizonEDA.imp", Gio::APPLICATION_NON_UNIQUE);
310
311 main_window = MainWindow::create();
312 canvas = main_window->canvas;
313 clipboard = ClipboardBase::create(*core);
314 clipboard_handler = std::make_unique<ClipboardHandler>(*clipboard);
315
316 canvas->signal_selection_changed().connect(sigc::mem_fun(*this, &ImpBase::handle_selection_changed));
317 canvas->signal_cursor_moved().connect(sigc::mem_fun(*this, &ImpBase::handle_cursor_move));
318 canvas->signal_button_press_event().connect(sigc::mem_fun(*this, &ImpBase::handle_click));
319 canvas->signal_button_press_event().connect(
320 [this](GdkEventButton *ev) {
321 if (property_panel_has_focus()
322 && (ev->button == 1 || ev->button == 3)) { // eat event so that things don't get deselected
323 canvas->grab_focus();
324 return true;
325 }
326 else {
327 return false;
328 }
329 },
330 false);
331 canvas->signal_button_release_event().connect(sigc::mem_fun(*this, &ImpBase::handle_click_release));
332 canvas->signal_request_display_name().connect(sigc::mem_fun(*this, &ImpBase::get_complete_display_name));
333
334 init_key();
335
336 {
337 selection_qualifiers = {
338 {CanvasGL::SelectionTool::BOX, CanvasGL::SelectionQualifier::INCLUDE_ORIGIN},
339 {CanvasGL::SelectionTool::LASSO, CanvasGL::SelectionQualifier::INCLUDE_ORIGIN},
340 {CanvasGL::SelectionTool::PAINT, CanvasGL::SelectionQualifier::TOUCH_BOX},
341 };
342
343 Gtk::RadioButton *selection_tool_box_button, *selection_tool_lasso_button, *selection_tool_paint_button;
344 Gtk::RadioButton *selection_qualifier_include_origin_button, *selection_qualifier_touch_box_button,
345 *selection_qualifier_include_box_button, *selection_qualifier_auto_button;
346 GET_WIDGET(selection_tool_box_button);
347 GET_WIDGET(selection_tool_lasso_button);
348 GET_WIDGET(selection_tool_paint_button);
349 GET_WIDGET(selection_qualifier_include_origin_button);
350 GET_WIDGET(selection_qualifier_touch_box_button);
351 GET_WIDGET(selection_qualifier_include_box_button);
352 GET_WIDGET(selection_qualifier_auto_button);
353
354 Gtk::Box *selection_qualifier_box;
355 GET_WIDGET(selection_qualifier_box);
356
357 std::map<CanvasGL::SelectionQualifier, Gtk::RadioButton *> qual_map = {
358 {CanvasGL::SelectionQualifier::INCLUDE_BOX, selection_qualifier_include_box_button},
359 {CanvasGL::SelectionQualifier::TOUCH_BOX, selection_qualifier_touch_box_button},
360 {CanvasGL::SelectionQualifier::INCLUDE_ORIGIN, selection_qualifier_include_origin_button},
361 {CanvasGL::SelectionQualifier::AUTO, selection_qualifier_auto_button}};
362 bind_widget<CanvasGL::SelectionQualifier>(qual_map, canvas->selection_qualifier, [this](auto v) {
363 selection_qualifiers.at(canvas->selection_tool) = v;
364 this->update_selection_label();
365 });
366
367 std::map<CanvasGL::SelectionTool, Gtk::RadioButton *> tool_map = {
368 {CanvasGL::SelectionTool::BOX, selection_tool_box_button},
369 {CanvasGL::SelectionTool::LASSO, selection_tool_lasso_button},
370 {CanvasGL::SelectionTool::PAINT, selection_tool_paint_button},
371 };
372 bind_widget<CanvasGL::SelectionTool>(
373 tool_map, canvas->selection_tool, [this, selection_qualifier_box, qual_map](auto v) {
374 this->update_selection_label();
375 selection_qualifier_box->set_sensitive(v != CanvasGL::SelectionTool::PAINT);
376 qual_map.at(CanvasGL::SelectionQualifier::AUTO)->set_sensitive(v != CanvasGL::SelectionTool::LASSO);
377
378 auto qual = selection_qualifiers.at(v);
379 qual_map.at(qual)->set_active(true);
380 });
381
382 connect_action(ActionID::SELECTION_TOOL_BOX,
383 [selection_tool_box_button](const auto &a) { selection_tool_box_button->set_active(true); });
384
385 connect_action(ActionID::SELECTION_TOOL_LASSO,
386 [selection_tool_lasso_button](const auto &a) { selection_tool_lasso_button->set_active(true); });
387
388 connect_action(ActionID::SELECTION_TOOL_PAINT,
389 [selection_tool_paint_button](const auto &a) { selection_tool_paint_button->set_active(true); });
390
391 connect_action(ActionID::SELECTION_QUALIFIER_AUTO, [this, selection_qualifier_auto_button](const auto &a) {
392 if (canvas->selection_tool == CanvasGL::SelectionTool::BOX)
393 selection_qualifier_auto_button->set_active(true);
394 });
395
396 connect_action(ActionID::SELECTION_QUALIFIER_INCLUDE_BOX,
397 [this, selection_qualifier_include_box_button](const auto &a) {
398 if (canvas->selection_tool != CanvasGL::SelectionTool::PAINT)
399 selection_qualifier_include_box_button->set_active(true);
400 });
401
402 connect_action(ActionID::SELECTION_QUALIFIER_INCLUDE_ORIGIN,
403 [this, selection_qualifier_include_origin_button](const auto &a) {
404 if (canvas->selection_tool != CanvasGL::SelectionTool::PAINT)
405 selection_qualifier_include_origin_button->set_active(true);
406 });
407
408 connect_action(ActionID::SELECTION_QUALIFIER_TOUCH_BOX, [selection_qualifier_touch_box_button](const auto &a) {
409 selection_qualifier_touch_box_button->set_active(true);
410 });
411
412
413 update_selection_label();
414 }
415
416 canvas->signal_selection_mode_changed().connect([this](auto mode) {
417 set_action_sensitive(make_action(ActionID::CLICK_SELECT), mode == CanvasGL::SelectionMode::HOVER);
418 if (mode == CanvasGL::SelectionMode::HOVER) {
419 main_window->selection_mode_label->set_text("Hover select");
420 }
421 else {
422 main_window->selection_mode_label->set_text("Click select");
423 }
424 });
425 canvas->set_selection_mode(CanvasGL::SelectionMode::HOVER);
426
427 connect_action(ActionID::CLICK_SELECT, [this](const auto &c) {
428 canvas->set_selection({});
429 canvas->set_selection_mode(CanvasGL::SelectionMode::NORMAL);
430 });
431
432 panels = Gtk::manage(new PropertyPanels(core));
433 panels->show_all();
434 panels->property_margin() = 10;
435 main_window->property_viewport->add(*panels);
436 panels->signal_update().connect([this] {
437 canvas_update();
438 canvas->set_selection(panels->get_selection(), false);
439 });
440 panels->signal_activate().connect([this] { canvas->grab_focus(); });
441 panels->signal_throttled().connect(
442 [this](bool thr) { main_window->property_throttled_revealer->set_reveal_child(thr); });
443
444 warnings_box = Gtk::manage(new WarningsBox());
445 warnings_box->signal_selected().connect(sigc::mem_fun(*this, &ImpBase::handle_warning_selected));
446 main_window->left_panel->pack_end(*warnings_box, false, false, 0);
447
448 selection_filter_dialog =
449 std::make_unique<SelectionFilterDialog>(this->main_window, canvas->selection_filter, *this);
450 selection_filter_dialog->signal_changed().connect(sigc::mem_fun(*this, &ImpBase::update_view_hints));
451
452 distraction_free_action = main_window->add_action_bool("distraction_free", distraction_free);
453 distraction_free_action->signal_change_state().connect([this](const Glib::VariantBase &v) {
454 auto b = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(v).get();
455 if (b != distraction_free) {
456 trigger_action(ActionID::DISTRACTION_FREE);
457 }
458 });
459
460 connect_action(ActionID::DISTRACTION_FREE, [this](const auto &a) {
461 distraction_free = !distraction_free;
462 g_simple_action_set_state(distraction_free_action->gobj(), g_variant_new_boolean(distraction_free));
463 if (distraction_free) {
464 left_panel_width = main_window->left_panel->get_allocated_width()
465 + main_window->left_panel->get_margin_start()
466 + main_window->left_panel->get_margin_end();
467 }
468
469 auto [scale, offset] = canvas->get_scale_and_offset();
470 if (distraction_free)
471 offset.x += left_panel_width;
472 else
473 offset.x -= left_panel_width;
474 canvas->set_scale_and_offset(scale, offset);
475
476 main_window->left_panel->set_visible(!distraction_free);
477 bool show_properties = panels->get_selection().size() > 0;
478 main_window->property_scrolled_window->set_visible(show_properties && !distraction_free);
479 this->update_view_hints();
480 });
481 connect_action(ActionID::SELECTION_FILTER, [this](const auto &a) { selection_filter_dialog->present(); });
482 connect_action(ActionID::SAVE, [this](const auto &a) {
483 if (!read_only) {
484 core->save();
485 json j;
486 this->get_save_meta(j);
487 if (!j.is_null()) {
488 save_json_to_file(core->get_filename() + meta_suffix, j);
489 }
490 main_window->set_version_info("");
491 }
492 });
493 connect_action(ActionID::UNDO, [this](const auto &a) {
494 core->undo();
495 this->canvas_update_from_pp();
496 this->update_property_panels();
497 });
498 connect_action(ActionID::REDO, [this](const auto &a) {
499 core->redo();
500 this->canvas_update_from_pp();
501 this->update_property_panels();
502 });
503
504 connect_action(ActionID::RELOAD_POOL, [this](const auto &a) {
505 core->reload_pool();
506 this->canvas_update_from_pp();
507 });
508
509 connect_action(ActionID::COPY, [this](const auto &a) {
510 clipboard_handler->copy(canvas->get_selection(), canvas->get_cursor_pos());
511 });
512
513 connect_action(ActionID::VIEW_ALL, [this](const auto &a) {
514 auto bbox = canvas->get_bbox();
515 std::cout << coord_to_string(bbox.first) << " " << coord_to_string(bbox.second) << std::endl;
516 canvas->zoom_to_bbox(bbox.first, bbox.second);
517 });
518
519 connect_action(ActionID::POPOVER, [this](const auto &a) {
520 Gdk::Rectangle rect;
521 auto c = canvas->get_cursor_pos_win();
522 rect.set_x(c.x);
523 rect.set_y(c.y);
524 tool_popover->set_pointing_to(rect);
525
526 this->update_action_sensitivity();
527 std::map<ActionToolID, bool> can_begin;
528 auto sel = canvas->get_selection();
529 for (const auto &it : action_catalog) {
530 if (it.first.first == ActionID::TOOL) {
531 bool r = core->tool_can_begin(it.first.second, sel).first;
532 can_begin[it.first] = r;
533 }
534 else {
535 can_begin[it.first] = this->get_action_sensitive(it.first);
536 }
537 }
538 tool_popover->set_can_begin(can_begin);
539
540 #if GTK_CHECK_VERSION(3, 22, 0)
541 tool_popover->popup();
542 #else
543 tool_popover->show();
544 #endif
545 });
546
547 connect_action(ActionID::PREFERENCES, [this](const auto &a) { show_preferences(); });
548
549 init_action();
550
551 connect_action(ActionID::VIEW_TOP, [this](const auto &a) { set_flip_view(false); });
552
553 connect_action(ActionID::VIEW_BOTTOM, [this](const auto &a) { set_flip_view(true); });
554
555 connect_action(ActionID::FLIP_VIEW, [this](const auto &a) { set_flip_view(!canvas->get_flip_view()); });
556
557 connect_action(ActionID::SELECT_POLYGON, sigc::mem_fun(*this, &ImpBase::handle_select_polygon));
558
559 bottom_view_action = main_window->add_action_bool("bottom_view", false);
560 bottom_view_action->signal_change_state().connect([this](const Glib::VariantBase &v) {
561 auto b = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(v).get();
562 if (b) {
563 trigger_action(ActionID::VIEW_BOTTOM);
564 }
565 else {
566 trigger_action(ActionID::VIEW_TOP);
567 }
568 });
569
570
571 canvas->signal_can_steal_focus().connect([this](bool &can_steal) {
572 can_steal = !(main_window->search_entry->property_has_focus() || property_panel_has_focus());
573 });
574
575 init_search();
576
577 if (auto grid_settings = core->get_grid_settings()) {
578 if (!m_meta.is_null()) {
579 if (m_meta.count("grid_settings")) {
580 const auto &s = m_meta.at("grid_settings");
581 auto &cur = grid_settings->current;
582 cur.spacing_square = s.at("spacing_square").get<uint64_t>();
583 cur.origin.x = s.at("origin_x").get<int64_t>();
584 cur.origin.y = s.at("origin_y").get<int64_t>();
585 cur.spacing_rect.x = s.at("spacing_x").get<int64_t>();
586 cur.spacing_rect.y = s.at("spacing_y").get<int64_t>();
587 if (s.at("mode").get<std::string>() == "rect") {
588 cur.mode = GridSettings::Grid::Mode::RECTANGULAR;
589 }
590 else {
591 cur.mode = GridSettings::Grid::Mode::SQUARE;
592 }
593 }
594 else {
595 grid_settings->current.spacing_square = m_meta.value("grid_spacing", 1.25_mm);
596 }
597 }
598 }
599
600 grid_controller.emplace(*main_window, *canvas, core->get_grid_settings());
601 connect_action(ActionID::SET_GRID_ORIGIN, [this](const auto &c) {
602 auto co = canvas->get_cursor_pos();
603 grid_controller->set_origin(co);
604 });
605
606 if (auto grid_settings = core->get_grid_settings()) {
607 grids_window = GridsWindow::create(main_window, *grid_controller, *grid_settings);
608 connect_action(ActionID::GRIDS_WINDOW, [this](const auto &c) {
609 grids_window->set_select_mode(false);
610 grids_window->present();
611 });
612 connect_action(ActionID::SELECT_GRID, [this](const auto &c) {
613 grids_window->set_select_mode(true);
614 grids_window->present();
615 });
616 main_window->grid_window_button->signal_clicked().connect([this] { trigger_action(ActionID::GRIDS_WINDOW); });
617 grids_window->signal_changed().connect([this] {
618 core->set_needs_save();
619 set_action_sensitive(make_action(ActionID::SELECT_GRID), grids_window->has_grids());
620 });
621 set_action_sensitive(make_action(ActionID::SELECT_GRID), grids_window->has_grids());
622 }
623
624 auto save_button = create_action_button(make_action(ActionID::SAVE));
625 save_button->show();
626 main_window->header->pack_start(*save_button);
627 core->signal_needs_save().connect([this](bool v) {
628 update_action_sensitivity();
629 json j;
630 j["op"] = "needs-save";
631 j["pid"] = getpid();
632 j["filename"] = core->get_filename();
633 j["needs_save"] = core->get_needs_save();
634 send_json(j);
635 });
636 set_action_sensitive(make_action(ActionID::SAVE), false);
637
638 {
639 auto undo_redo_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
640 undo_redo_box->get_style_context()->add_class("linked");
641
642 auto undo_button = create_action_button(make_action(ActionID::UNDO));
643 gtk_button_set_label(undo_button->gobj(), NULL);
644 undo_button->set_tooltip_text("Undo");
645 undo_button->set_image_from_icon_name("edit-undo-symbolic", Gtk::ICON_SIZE_BUTTON);
646 undo_redo_box->pack_start(*undo_button, false, false, 0);
647
648 auto redo_button = create_action_button(make_action(ActionID::REDO));
649 gtk_button_set_label(redo_button->gobj(), NULL);
650 redo_button->set_tooltip_text("Redo");
651 redo_button->set_image_from_icon_name("edit-redo-symbolic", Gtk::ICON_SIZE_BUTTON);
652 undo_redo_box->pack_start(*redo_button, false, false, 0);
653
654 undo_redo_box->show_all();
655 main_window->header->pack_start(*undo_redo_box);
656 }
657
658 core->signal_can_undo_redo().connect([this] { update_action_sensitivity(); });
659 canvas->signal_selection_changed().connect([this] {
660 if (!core->tool_is_active()) {
661 update_action_sensitivity();
662 }
663 });
664 core->signal_can_undo_redo().emit();
665
666 core->signal_load_tool_settings().connect([this](ToolID id) {
667 json j;
668 auto fn = get_tool_settings_filename(id);
669 if (Glib::file_test(fn, Glib::FILE_TEST_IS_REGULAR)) {
670 j = load_json_from_file(fn);
671 }
672 return j;
673 });
674 core->signal_save_tool_settings().connect(
675 [this](ToolID id, json j) { save_json_to_file(get_tool_settings_filename(id), j); });
676
677 if (core->get_rules()) {
678 rules_window = RulesWindow::create(main_window, *canvas, *core);
679 rules_window->signal_canvas_update().connect(sigc::mem_fun(*this, &ImpBase::canvas_update_from_pp));
680 rules_window->signal_changed().connect([this] { core->set_needs_save(); });
681 core->signal_tool_changed().connect([this](ToolID id) { rules_window->set_enabled(id == ToolID::NONE); });
682
683 connect_action(ActionID::RULES, [this](const auto &conn) { rules_window->present(); });
684 connect_action(ActionID::RULES_RUN_CHECKS, [this](const auto &conn) {
685 rules_window->run_checks();
686 rules_window->present();
687 });
688 connect_action(ActionID::RULES_APPLY, [this](const auto &conn) { rules_window->apply_rules(); });
689
690 {
691 auto button = create_action_button(make_action(ActionID::RULES));
692 button->set_label("Rules…");
693 main_window->header->pack_start(*button);
694 button->show();
695 }
696 }
697
698 tool_popover = Gtk::manage(new ToolPopover(canvas, get_editor_type_for_action()));
699 tool_popover->set_position(Gtk::POS_BOTTOM);
700 tool_popover->signal_action_activated().connect(
701 [this](ActionID action_id, ToolID tool_id) { trigger_action(std::make_pair(action_id, tool_id)); });
702
703
704 log_window = new LogWindow();
705 log_window->set_transient_for(*main_window);
706 Logger::get().set_log_handler([this](const Logger::Item &it) { log_window->get_view()->push_log(it); });
707
708 main_window->add_action("view_log", [this] { log_window->present(); });
709
710 main_window->signal_delete_event().connect(sigc::mem_fun(*this, &ImpBase::handle_close));
711
712 for (const auto &la : core->get_layer_provider().get_layers()) {
713 canvas->set_layer_display(la.first, LayerDisplay(true, LayerDisplay::Mode::FILL));
714 }
715
716
717 preferences.load();
718
719 preferences_monitor = Gio::File::create_for_path(Preferences::get_preferences_filename())->monitor();
720
721 preferences_monitor->signal_changed().connect([this](const Glib::RefPtr<Gio::File> &file,
722 const Glib::RefPtr<Gio::File> &file_other,
723 Gio::FileMonitorEvent ev) {
724 if (ev == Gio::FILE_MONITOR_EVENT_CHANGES_DONE_HINT)
725 preferences.load();
726 });
727 PreferencesProvider::get().set_prefs(preferences);
728
729 add_hamburger_menu();
730
731 auto view_options_popover = Gtk::manage(new Gtk::PopoverMenu);
732 main_window->view_options_button->set_popover(*view_options_popover);
733 {
734 Gdk::Rectangle rect;
735 rect.set_width(24);
736 main_window->view_options_button->get_popover()->set_pointing_to(rect);
737 }
738
739 view_options_menu = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
740 view_options_menu->set_border_width(9);
741 view_options_popover->add(*view_options_menu);
742 view_options_menu->show();
743 view_options_popover->child_property_submenu(*view_options_menu) = "main";
744
745 view_options_menu_append_action("Distraction free mode", "win.distraction_free");
746 add_tool_action(ActionID::SELECTION_FILTER, "selection_filter");
747 view_options_menu_append_action("Selection filter", "win.selection_filter");
748
749 imp_interface = std::make_unique<ImpInterface>(this);
750
751 construct();
752
753 {
754 auto refBuilder = Gtk::Builder::create();
755 refBuilder->add_from_resource("/org/horizon-eda/horizon/imp/app_menu.ui");
756
757 auto object = refBuilder->get_object("appmenu");
758 auto app_menu = Glib::RefPtr<Gio::MenuModel>::cast_dynamic(object);
759 hamburger_menu->append_section(app_menu);
760 }
761
762 if (sockets_connected)
763 main_window->add_action("preferences", [this] { trigger_action(ActionID::PREFERENCES); });
764
765 main_window->add_action("about", [this] {
766 AboutDialog dia;
767 dia.set_transient_for(*main_window);
768 dia.run();
769 });
770
771 main_window->add_action("help", [this] { trigger_action(ActionID::HELP); });
772
773 preferences.signal_changed().connect(sigc::mem_fun(*this, &ImpBase::apply_preferences));
774
775 apply_preferences();
776
777 canvas->property_work_layer().signal_changed().connect([this] {
778 if (core->tool_is_active()) {
779 ToolArgs args;
780 args.type = ToolEventType::LAYER_CHANGE;
781 args.coords = canvas->get_cursor_pos();
782 args.work_layer = canvas->property_work_layer();
783 ToolResponse r = core->tool_update(args);
784 tool_process(r);
785 }
786 selection_filter_dialog->set_work_layer(canvas->property_work_layer());
787 });
788
789 canvas->signal_grid_mul_changed().connect([this](unsigned int mul) {
790 std::string s = "×";
791 std::string n = std::to_string(mul);
792 for (int i = 0; i < (3 - (int)n.size()); i++) {
793 s += " ";
794 }
795 main_window->grid_mul_label->set_text(s + n);
796 });
797
798 context_menu = Gtk::manage(new Gtk::Menu());
799
800 core->signal_tool_changed().connect([this](ToolID id) { s_signal_action_sensitive.emit(); });
801
802 canvas->signal_hover_selection_changed().connect(sigc::mem_fun(*this, &ImpBase::hud_update));
803 canvas->signal_selection_changed().connect(sigc::mem_fun(*this, &ImpBase::hud_update));
804
805 canvas_update();
806
807 initial_view_all_conn = canvas->signal_size_allocate().connect([this](const Gtk::Allocation &alloc) {
808 auto bbox = canvas->get_bbox();
809 canvas->zoom_to_bbox(bbox.first, bbox.second);
810 initial_view_all_conn.disconnect();
811 });
812
813 handle_cursor_move(Coordi()); // fixes label
814
815 Gtk::IconTheme::get_default()->add_resource_path("/org/horizon-eda/horizon/icons");
816
817 Gtk::Window::set_default_icon_name("horizon-eda");
818
819 Glib::set_prgname("horizon-eda"); // Fixes icons on wayland
820
821 app->signal_startup().connect([this, app] {
822 app->add_window(*main_window);
823 app->add_action("quit", [this] { main_window->close(); });
824 });
825
826 auto cssp = Gtk::CssProvider::create();
827 cssp->load_from_resource("/org/horizon-eda/horizon/global.css");
828 Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), cssp, 700);
829
830 canvas->signal_motion_notify_event().connect([this](GdkEventMotion *ev) {
831 handle_drag();
832 return false;
833 });
834
835 canvas->signal_button_release_event().connect([this](GdkEventButton *ev) {
836 selection_for_drag_move.clear();
837 return false;
838 });
839
840 handle_selection_changed();
841
842 core->signal_rebuilt().connect([this] { update_monitor(); });
843
844 Glib::signal_timeout().connect_seconds(sigc::track_obj(
845 [this] {
846 if (core->tool_is_active()) {
847 queue_autosave = true;
848 }
849 else {
850 if (needs_autosave) {
851 core->autosave();
852 needs_autosave = false;
853 }
854 }
855 return true;
856 },
857 *main_window),
858 60);
859 core->signal_rebuilt().connect([this] {
860 if (queue_autosave) {
861 queue_autosave = false;
862 core->autosave();
863 needs_autosave = false;
864 }
865 });
866 core->signal_needs_save().connect([this](bool v) {
867 if (!v)
868 needs_autosave = false;
869 });
870 core->signal_modified().connect([this] { needs_autosave = true; });
871
872 if (core->get_block()) {
873 core->signal_rebuilt().connect(sigc::mem_fun(*this, &ImpBase::set_window_title_from_block));
874 set_window_title_from_block();
875 }
876 update_view_hints();
877
878 reset_tool_hint_label();
879
880 check_version();
881
882 {
883 json j;
884 j["op"] = "ready";
885 j["pid"] = getpid();
886 send_json(j);
887 }
888
889 app->run(*main_window);
890 }
891
show_preferences(std::optional<std::string> page)892 void ImpBase::show_preferences(std::optional<std::string> page)
893 {
894 json j;
895 j["op"] = "preferences";
896 if (page) {
897 j["page"] = *page;
898 }
899 allow_set_foreground_window(mgr_pid);
900 this->send_json(j);
901 }
902
parameter_window_add_polygon_expand(ParameterWindow * parameter_window)903 void ImpBase::parameter_window_add_polygon_expand(ParameterWindow *parameter_window)
904 {
905 auto button = Gtk::manage(new Gtk::Button("Polygon expand"));
906 parameter_window->add_button(button);
907 button->signal_clicked().connect([this, parameter_window] {
908 auto sel = canvas->get_selection();
909 if (sel.size() == 1) {
910 auto &s = *sel.begin();
911 if (s.type == ObjectType::POLYGON_EDGE || s.type == ObjectType::POLYGON_VERTEX) {
912 auto poly = core->get_polygon(s.uuid);
913 if (!poly->has_arcs()) {
914 std::stringstream ss;
915 ss.imbue(std::locale::classic());
916 ss << "expand-polygon [ " << poly->parameter_class << " ";
917 for (const auto &it : poly->vertices) {
918 ss << it.position.x << " " << it.position.y << " ";
919 }
920 ss << "]\n";
921 parameter_window->insert_text(ss.str());
922 }
923 }
924 }
925 });
926 }
927
928
edit_pool_item(ObjectType type,const UUID & uu)929 void ImpBase::edit_pool_item(ObjectType type, const UUID &uu)
930 {
931 json j;
932 j["op"] = "edit";
933 j["type"] = object_type_lut.lookup_reverse(type);
934 j["uuid"] = static_cast<std::string>(uu);
935 send_json(j);
936 }
937
get_tool_for_drag_move(bool ctrl,const std::set<SelectableRef> & sel) const938 ToolID ImpBase::get_tool_for_drag_move(bool ctrl, const std::set<SelectableRef> &sel) const
939 {
940 if (ctrl)
941 return ToolID::DUPLICATE;
942
943 if (preferences.mouse.drag_polygon_edges && sel.size() == 1 && sel.begin()->type == ObjectType::POLYGON_EDGE)
944 return ToolID::DRAG_POLYGON_EDGE;
945
946 if (preferences.mouse.drag_to_move)
947 return ToolID::MOVE;
948 else
949 return ToolID::NONE;
950 }
951
handle_drag()952 void ImpBase::handle_drag()
953 {
954 if (drag_tool == ToolID::NONE)
955 return;
956 if (selection_for_drag_move.size() == 0)
957 return;
958 auto pos = canvas->get_cursor_pos_win();
959 auto delta = pos - cursor_pos_drag_begin;
960 if (delta.mag_sq() > (50 * 50)) {
961 highlights.clear();
962 update_highlights();
963 ToolArgs args;
964 args.coords = cursor_pos_grid_drag_begin;
965 args.selection = selection_for_drag_move;
966
967 ToolResponse r = core->tool_begin(drag_tool, args, imp_interface.get(), true);
968 tool_process(r);
969
970 selection_for_drag_move.clear();
971 drag_tool = ToolID::NONE;
972 }
973 }
974
apply_preferences()975 void ImpBase::apply_preferences()
976 {
977 const auto &canvas_prefs = get_canvas_preferences();
978 canvas->set_appearance(canvas_prefs.appearance);
979 if (core->tool_is_active()) {
980 canvas->set_cursor_size(canvas_prefs.appearance.cursor_size_tool);
981 }
982 canvas->show_all_junctions_in_schematic = preferences.schematic.show_all_junctions;
983
984 auto av = get_editor_type_for_action();
985 for (auto &it : action_connections) {
986 auto act = action_catalog.at(it.first);
987 if (!(act.flags & ActionCatalogItem::FLAGS_NO_PREFERENCES) && preferences.key_sequences.keys.count(it.first)) {
988 auto pref = preferences.key_sequences.keys.at(it.first);
989 std::vector<KeySequence> *seqs = nullptr;
990 if (pref.count(av) && pref.at(av).size()) {
991 seqs = &pref.at(av);
992 }
993 else if (pref.count(ActionCatalogItem::AVAILABLE_EVERYWHERE)
994 && pref.at(ActionCatalogItem::AVAILABLE_EVERYWHERE).size()) {
995 seqs = &pref.at(ActionCatalogItem::AVAILABLE_EVERYWHERE);
996 }
997 if (seqs) {
998 it.second.key_sequences = *seqs;
999 }
1000 else {
1001 it.second.key_sequences.clear();
1002 }
1003 }
1004 }
1005 in_tool_key_sequeces_preferences = preferences.in_tool_key_sequences;
1006 in_tool_key_sequeces_preferences.keys.erase(InToolActionID::LMB);
1007 in_tool_key_sequeces_preferences.keys.erase(InToolActionID::RMB);
1008 in_tool_key_sequeces_preferences.keys.erase(InToolActionID::LMB_RELEASE);
1009 apply_arrow_keys();
1010
1011 {
1012 const auto mod0 = static_cast<GdkModifierType>(0);
1013
1014 in_tool_key_sequeces_preferences.keys[InToolActionID::CANCEL] = {{{GDK_KEY_Escape, mod0}}};
1015 in_tool_key_sequeces_preferences.keys[InToolActionID::COMMIT] = {{{GDK_KEY_Return, mod0}},
1016 {{GDK_KEY_KP_Enter, mod0}}};
1017 }
1018
1019 key_sequence_dialog->clear();
1020 for (const auto &it : action_connections) {
1021 if (it.second.key_sequences.size()) {
1022 key_sequence_dialog->add_sequence(it.second.key_sequences, action_catalog.at(it.first).name);
1023 tool_popover->set_key_sequences(it.first, it.second.key_sequences);
1024 }
1025 }
1026 preferences_apply_to_canvas(canvas, preferences);
1027 for (auto it : action_buttons) {
1028 it->update_key_sequences();
1029 it->set_keep_primary_action(!preferences.action_bar.remember);
1030 }
1031 main_window->set_use_action_bar(preferences.action_bar.enable);
1032 }
1033
canvas_update_from_pp()1034 void ImpBase::canvas_update_from_pp()
1035 {
1036 if (core->tool_is_active()) {
1037 canvas_update();
1038 canvas->set_selection(core->get_tool_selection());
1039 return;
1040 }
1041
1042 auto sel = canvas->get_selection();
1043 canvas_update();
1044 canvas->set_selection(sel, false);
1045 update_highlights();
1046 }
1047
tool_begin(ToolID id,bool override_selection,const std::set<SelectableRef> & sel,std::unique_ptr<ToolData> data)1048 void ImpBase::tool_begin(ToolID id, bool override_selection, const std::set<SelectableRef> &sel,
1049 std::unique_ptr<ToolData> data)
1050 {
1051 if (core->tool_is_active()) {
1052 Logger::log_critical("can't begin tool while tool is active", Logger::Domain::IMP);
1053 return;
1054 }
1055 highlights.clear();
1056 update_highlights();
1057 ToolArgs args;
1058 args.data = std::move(data);
1059 args.coords = canvas->get_cursor_pos();
1060
1061 if (override_selection)
1062 args.selection = sel;
1063 else
1064 args.selection = canvas->get_selection();
1065
1066 args.work_layer = canvas->property_work_layer();
1067 ToolResponse r = core->tool_begin(id, args, imp_interface.get());
1068 tool_process(r);
1069 }
1070
add_hamburger_menu()1071 void ImpBase::add_hamburger_menu()
1072 {
1073 auto hamburger_button = Gtk::manage(new Gtk::MenuButton);
1074 hamburger_button->set_image_from_icon_name("open-menu-symbolic", Gtk::ICON_SIZE_BUTTON);
1075 core->signal_tool_changed().connect(
1076 [hamburger_button](ToolID t) { hamburger_button->set_sensitive(t == ToolID::NONE); });
1077
1078 hamburger_menu = Gio::Menu::create();
1079 hamburger_button->set_menu_model(hamburger_menu);
1080 hamburger_button->show();
1081 main_window->header->pack_end(*hamburger_button);
1082 }
1083
layer_up_down(bool up)1084 void ImpBase::layer_up_down(bool up)
1085 {
1086 int wl = canvas->property_work_layer();
1087 auto layers = core->get_layer_provider().get_layers();
1088 std::vector<int> layer_indexes;
1089 layer_indexes.reserve(layers.size());
1090 std::transform(layers.begin(), layers.end(), std::back_inserter(layer_indexes),
1091 [](const auto &x) { return x.first; });
1092
1093 int idx = std::find(layer_indexes.begin(), layer_indexes.end(), wl) - layer_indexes.begin();
1094 if (up) {
1095 idx++;
1096 }
1097 else {
1098 idx--;
1099 }
1100 if (idx >= 0 && idx < (int)layers.size()) {
1101 canvas->property_work_layer() = layer_indexes.at(idx);
1102 }
1103 }
1104
goto_layer(int layer)1105 void ImpBase::goto_layer(int layer)
1106 {
1107 if (core->get_layer_provider().get_layers().count(layer)) {
1108 canvas->property_work_layer() = layer;
1109 }
1110 }
1111
handle_cursor_move(const Coordi & pos)1112 void ImpBase::handle_cursor_move(const Coordi &pos)
1113 {
1114 if (core->tool_is_active()) {
1115 ToolArgs args;
1116 args.type = ToolEventType::MOVE;
1117 args.coords = pos;
1118 args.work_layer = canvas->property_work_layer();
1119 ToolResponse r = core->tool_update(args);
1120 tool_process(r);
1121 }
1122 main_window->cursor_label->set_text(coord_to_string(pos));
1123 }
1124
fix_cursor_pos()1125 void ImpBase::fix_cursor_pos()
1126 {
1127 auto ev = gtk_get_current_event();
1128 auto dev = gdk_event_get_device(ev);
1129 auto win = canvas->get_window()->gobj();
1130 gdouble x, y;
1131 gdk_window_get_device_position_double(win, dev, &x, &y, nullptr);
1132 if (!canvas->get_has_window()) {
1133 auto alloc = canvas->get_allocation();
1134 x -= alloc.get_x();
1135 y -= alloc.get_y();
1136 }
1137 canvas->update_cursor_pos(x, y);
1138 }
1139
handle_click_release(const GdkEventButton * button_event)1140 bool ImpBase::handle_click_release(const GdkEventButton *button_event)
1141 {
1142 if (core->tool_is_active() && button_event->button != 2 && !(button_event->state & Gdk::SHIFT_MASK)) {
1143 ToolArgs args;
1144 args.type = ToolEventType::ACTION;
1145 args.action = InToolActionID::LMB_RELEASE;
1146 args.coords = canvas->get_cursor_pos();
1147 args.target = canvas->get_current_target();
1148 args.work_layer = canvas->property_work_layer();
1149 ToolResponse r = core->tool_update(args);
1150 tool_process(r);
1151 }
1152 return false;
1153 }
1154
create_context_menu_item(ActionToolID act)1155 Gtk::MenuItem *ImpBase::create_context_menu_item(ActionToolID act)
1156 {
1157 auto it = Gtk::manage(new Gtk::MenuItem);
1158 auto box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 8));
1159
1160 {
1161 auto la = Gtk::manage(new Gtk::Label);
1162 la->set_xalign(0);
1163 la->set_label(action_catalog.at(act).name);
1164 box->pack_start(*la, false, false, 0);
1165 }
1166
1167 {
1168 auto la = Gtk::manage(new Gtk::Label);
1169 la->set_xalign(1);
1170 la->get_style_context()->add_class("dim-label");
1171 la->set_label(key_sequences_to_string(action_connections.at(act).key_sequences));
1172 la->set_margin_end(4);
1173 box->pack_start(*la, true, true, 0);
1174 }
1175
1176 box->show_all();
1177 it->add(*box);
1178 return it;
1179 }
1180
create_context_menu(Gtk::Menu * parent,const std::set<SelectableRef> & sel)1181 void ImpBase::create_context_menu(Gtk::Menu *parent, const std::set<SelectableRef> &sel)
1182 {
1183 update_action_sensitivity();
1184 Gtk::SeparatorMenuItem *sep = nullptr;
1185 for (const auto &it_gr : action_group_catalog) {
1186 std::vector<Gtk::MenuItem *> menu_items;
1187 for (const auto &it : action_catalog) {
1188 if (it.second.group == it_gr.first && (it.second.availability & get_editor_type_for_action())
1189 && !(it.second.flags & ActionCatalogItem::FLAGS_NO_MENU)) {
1190 if (it.first.first == ActionID::TOOL) {
1191 auto r = core->tool_can_begin(it.first.second, {sel});
1192 if (r.first && r.second) {
1193 auto la_sub = create_context_menu_item(it.first);
1194 ToolID tool_id = it.first.second;
1195 std::set<SelectableRef> sr(sel);
1196 la_sub->signal_activate().connect([this, tool_id, sr] {
1197 canvas->set_selection(sr, false);
1198 fix_cursor_pos();
1199 tool_begin(tool_id);
1200 });
1201 la_sub->show();
1202 menu_items.push_back(la_sub);
1203 }
1204 }
1205 else {
1206 if (get_action_sensitive(it.first) && (it.second.flags & ActionCatalogItem::FLAGS_SPECIFIC)) {
1207 auto la_sub = create_context_menu_item(it.first);
1208 ActionID action_id = it.first.first;
1209 std::set<SelectableRef> sr(sel);
1210 la_sub->signal_activate().connect([this, action_id, sr] {
1211 canvas->set_selection(sr, false);
1212 fix_cursor_pos();
1213 trigger_action(make_action(action_id));
1214 });
1215 la_sub->show();
1216 menu_items.push_back(la_sub);
1217 }
1218 }
1219 }
1220 }
1221 if (menu_items.size() > 6) {
1222 auto submenu = Gtk::manage(new Gtk::Menu);
1223 submenu->show();
1224 for (auto it : menu_items) {
1225 submenu->append(*it);
1226 }
1227 auto la_sub = Gtk::manage(new Gtk::MenuItem(it_gr.second));
1228 la_sub->show();
1229 la_sub->set_submenu(*submenu);
1230 parent->append(*la_sub);
1231 }
1232 else {
1233 for (auto it : menu_items) {
1234 parent->append(*it);
1235 }
1236 }
1237 if (menu_items.size()) {
1238 sep = Gtk::manage(new Gtk::SeparatorMenuItem);
1239 sep->show();
1240 parent->append(*sep);
1241 }
1242 }
1243 if (sep)
1244 delete sep;
1245 }
1246
reset_tool_hint_label()1247 void ImpBase::reset_tool_hint_label()
1248 {
1249 const auto act = make_action(ActionID::POPOVER);
1250 if (action_connections.count(act)) {
1251 if (action_connections.at(act).key_sequences.size()) {
1252 const auto keys = key_sequence_to_string(action_connections.at(act).key_sequences.front());
1253 main_window->tool_hint_label->set_text("> " + keys + " for menu");
1254 return;
1255 }
1256 }
1257 main_window->tool_hint_label->set_text(">");
1258 }
1259
handle_click(const GdkEventButton * button_event)1260 bool ImpBase::handle_click(const GdkEventButton *button_event)
1261 {
1262 if (button_event->button > 3) {
1263 handle_extra_button(button_event);
1264 return false;
1265 }
1266
1267 if (button_event->button != 2)
1268 set_search_mode(false);
1269
1270 main_window->key_hint_set_visible(false);
1271
1272 bool need_menu = false;
1273 if (core->tool_is_active() && button_event->button != 2 && !(button_event->state & Gdk::SHIFT_MASK)
1274 && button_event->type != GDK_2BUTTON_PRESS && button_event->type != GDK_3BUTTON_PRESS) {
1275 ToolArgs args;
1276 args.type = ToolEventType::ACTION;
1277 if (button_event->button == 1)
1278 args.action = InToolActionID::LMB;
1279 else
1280 args.action = InToolActionID::RMB;
1281 args.coords = canvas->get_cursor_pos();
1282 args.target = canvas->get_current_target();
1283 args.work_layer = canvas->property_work_layer();
1284 ToolResponse r = core->tool_update(args);
1285 tool_process(r);
1286 }
1287 else if (!core->tool_is_active() && button_event->type == GDK_2BUTTON_PRESS) {
1288 auto sel = canvas->get_selection();
1289 if (sel.size() == 1) {
1290 auto a = get_doubleclick_action(sel.begin()->type, sel.begin()->uuid);
1291 if (a.first != ActionID::NONE) {
1292 selection_for_drag_move.clear();
1293 trigger_action(a);
1294 }
1295 }
1296 }
1297 else if (!core->tool_is_active() && button_event->button == 1 && !(button_event->state & Gdk::SHIFT_MASK)) {
1298 handle_maybe_drag(button_event->state & Gdk::CONTROL_MASK);
1299 }
1300 else if (!core->tool_is_active() && button_event->button == 3) {
1301 for (const auto it : context_menu->get_children()) {
1302 delete it;
1303 }
1304 std::set<SelectableRef> sel_for_menu;
1305 if (canvas->get_selection_mode() == CanvasGL::SelectionMode::HOVER) {
1306 sel_for_menu = canvas->get_selection();
1307 }
1308 else {
1309 auto c = canvas->screen2canvas(Coordf(button_event->x, button_event->y));
1310 auto sel = canvas->get_selection_at(Coordi(c.x, c.y));
1311 auto sel_from_canvas = canvas->get_selection();
1312 std::set<SelectableRef> isect;
1313 std::set_intersection(sel.begin(), sel.end(), sel_from_canvas.begin(), sel_from_canvas.end(),
1314 std::inserter(isect, isect.begin()));
1315 if (isect.size()) { // was in selection
1316 sel_for_menu = sel_from_canvas;
1317 }
1318 else if (sel.size() == 1) { // there's exactly one item
1319 canvas->set_selection(sel, false);
1320 sel_for_menu = sel;
1321 }
1322 else if (sel.size() > 1) { // multiple items: do our own menu
1323 canvas->set_selection({}, false);
1324 for (const auto &sr : sel) {
1325 std::string text = get_complete_display_name(sr);
1326 auto la = Gtk::manage(new Gtk::MenuItem(text));
1327
1328 la->signal_select().connect([this, sr] { canvas->set_selection({sr}, false); });
1329
1330 la->signal_deselect().connect([this] { canvas->set_selection({}, false); });
1331
1332 auto submenu = Gtk::manage(new Gtk::Menu);
1333
1334 create_context_menu(submenu, {sr});
1335 la->set_submenu(*submenu);
1336 la->show();
1337 context_menu->append(*la);
1338 }
1339 need_menu = true;
1340 sel_for_menu.clear();
1341 }
1342 }
1343
1344 if (sel_for_menu.size()) {
1345 create_context_menu(context_menu, sel_for_menu);
1346 need_menu = true;
1347 }
1348 }
1349
1350 if (need_menu) {
1351 #if GTK_CHECK_VERSION(3, 22, 0)
1352 context_menu->popup_at_pointer((GdkEvent *)button_event);
1353 #else
1354 context_menu->popup(0, gtk_get_current_event_time());
1355 #endif
1356 }
1357 return false;
1358 }
1359
get_complete_display_name(const SelectableRef & sr)1360 std::string ImpBase::get_complete_display_name(const SelectableRef &sr)
1361 {
1362 std::string text = object_descriptions.at(sr.type).name;
1363 auto display_name = core->get_display_name(sr.type, sr.uuid);
1364 if (display_name.size()) {
1365 text += " " + display_name;
1366 }
1367 auto layers = core->get_layer_provider().get_layers();
1368 if (layers.count(sr.layer.start()) && layers.count(sr.layer.end())) {
1369 if (sr.layer.is_multilayer())
1370 text += " (" + layers.at(sr.layer.start()).name + +" - " + layers.at(sr.layer.end()).name + ")";
1371 else
1372 text += " (" + layers.at(sr.layer.start()).name + ")";
1373 }
1374 return text;
1375 }
1376
1377
handle_maybe_drag(bool ctrl)1378 void ImpBase::handle_maybe_drag(bool ctrl)
1379 {
1380 auto c = canvas->screen2canvas(canvas->get_cursor_pos_win());
1381 auto sel_at_cursor = canvas->get_selection_at(Coordi(c.x, c.y));
1382 auto sel_from_canvas = canvas->get_selection();
1383 std::set<SelectableRef> isect;
1384 std::set_intersection(sel_from_canvas.begin(), sel_from_canvas.end(), sel_at_cursor.begin(), sel_at_cursor.end(),
1385 std::inserter(isect, isect.begin()));
1386 if (isect.size()) {
1387 drag_tool = get_tool_for_drag_move(ctrl, sel_from_canvas);
1388 if (drag_tool != ToolID::NONE) {
1389 canvas->inhibit_drag_selection();
1390 cursor_pos_drag_begin = canvas->get_cursor_pos_win();
1391 cursor_pos_grid_drag_begin = canvas->get_cursor_pos();
1392 selection_for_drag_move = sel_from_canvas;
1393 }
1394 }
1395 }
1396
tool_process(ToolResponse & resp)1397 void ImpBase::tool_process(ToolResponse &resp)
1398 {
1399 if (!core->tool_is_active()) {
1400 imp_interface->dialogs.close_nonmodal();
1401 reset_tool_hint_label();
1402 canvas->set_cursor_external(false);
1403 canvas->snap_filter.clear();
1404 no_update = false;
1405 highlights.clear();
1406 update_highlights();
1407 }
1408 if (!no_update) {
1409 canvas_update();
1410 if (core->tool_is_active()) {
1411 canvas->set_selection(core->get_tool_selection());
1412 }
1413 else {
1414 if (canvas->get_selection_mode() == CanvasGL::SelectionMode::NORMAL) {
1415 canvas->set_selection(core->get_tool_selection());
1416 }
1417 else {
1418 canvas->set_selection({});
1419 }
1420 }
1421 }
1422 if (resp.next_tool != ToolID::NONE) {
1423 highlights.clear();
1424 update_highlights();
1425 ToolArgs args;
1426 args.coords = canvas->get_cursor_pos();
1427 args.keep_selection = true;
1428 args.data = std::move(resp.data);
1429 ToolResponse r = core->tool_begin(resp.next_tool, args, imp_interface.get());
1430 tool_process(r);
1431 }
1432 }
1433
expand_selection_for_property_panel(std::set<SelectableRef> & sel_extra,const std::set<SelectableRef> & sel)1434 void ImpBase::expand_selection_for_property_panel(std::set<SelectableRef> &sel_extra,
1435 const std::set<SelectableRef> &sel)
1436 {
1437 }
1438
handle_selection_changed(void)1439 void ImpBase::handle_selection_changed(void)
1440 {
1441 // std::cout << "Selection changed\n";
1442 // std::cout << "---" << std::endl;
1443 if (!core->tool_is_active()) {
1444 highlights.clear();
1445 update_highlights();
1446 update_property_panels();
1447 }
1448 if (sockets_connected) {
1449 auto selection = canvas->get_selection();
1450 if (selection != last_canvas_selection) {
1451 handle_selection_cross_probe();
1452 }
1453 last_canvas_selection = selection;
1454 }
1455 }
1456
update_property_panels()1457 void ImpBase::update_property_panels()
1458 {
1459 auto sel = canvas->get_selection();
1460 decltype(sel) sel_extra;
1461 for (const auto &it : sel) {
1462 switch (it.type) {
1463 case ObjectType::POLYGON_EDGE:
1464 case ObjectType::POLYGON_VERTEX: {
1465 sel_extra.emplace(it.uuid, ObjectType::POLYGON);
1466 auto poly = core->get_polygon(it.uuid);
1467 if (poly->usage && poly->usage->get_type() == PolygonUsage::Type::PLANE) {
1468 sel_extra.emplace(poly->usage->get_uuid(), ObjectType::PLANE);
1469 }
1470 if (poly->usage && poly->usage->get_type() == PolygonUsage::Type::KEEPOUT) {
1471 sel_extra.emplace(poly->usage->get_uuid(), ObjectType::KEEPOUT);
1472 }
1473 } break;
1474 default:;
1475 }
1476 }
1477 expand_selection_for_property_panel(sel_extra, sel);
1478
1479 sel.insert(sel_extra.begin(), sel_extra.end());
1480 panels->update_objects(sel);
1481 bool show_properties = panels->get_selection().size() > 0;
1482 main_window->property_scrolled_window->set_visible(show_properties && !distraction_free);
1483 }
1484
handle_tool_change(ToolID id)1485 void ImpBase::handle_tool_change(ToolID id)
1486 {
1487 panels->set_sensitive(id == ToolID::NONE);
1488 canvas->set_selection_allowed(id == ToolID::NONE);
1489 main_window->tool_bar_set_use_actions(core->get_tool_actions().size());
1490 if (id != ToolID::NONE) {
1491 main_window->tool_bar_set_tool_name(action_catalog.at({ActionID::TOOL, id}).name);
1492 main_window->tool_bar_set_tool_tip("");
1493 main_window->hud_hide();
1494 canvas->set_cursor_size(get_canvas_preferences().appearance.cursor_size_tool);
1495 }
1496 else {
1497 canvas->set_cursor_size(get_canvas_preferences().appearance.cursor_size);
1498 }
1499 main_window->tool_bar_set_visible(id != ToolID::NONE);
1500 tool_bar_clear_actions();
1501 main_window->action_bar_revealer->set_reveal_child(preferences.action_bar.show_in_tool || id == ToolID::NONE);
1502 update_cursor(id);
1503 }
1504
handle_warning_selected(const Coordi & pos)1505 void ImpBase::handle_warning_selected(const Coordi &pos)
1506 {
1507 canvas->center_and_zoom(pos);
1508 }
1509
handle_broadcast(const json & j)1510 bool ImpBase::handle_broadcast(const json &j)
1511 {
1512 const std::string op = j.at("op");
1513 guint32 timestamp = j.value("time", 0);
1514 if (op == "present") {
1515 main_window->present(timestamp);
1516 return true;
1517 }
1518 else if (op == "save") {
1519 force_end_tool();
1520 trigger_action(ActionID::SAVE);
1521 return true;
1522 }
1523 else if (op == "close") {
1524 core->delete_autosave();
1525 delete main_window;
1526 return true;
1527 }
1528 else if (op == "preferences") {
1529 const auto &prefs = j.at("preferences");
1530 preferences.load_from_json(prefs);
1531 preferences.signal_changed().emit();
1532 return true;
1533 }
1534 return false;
1535 }
1536
set_monitor_files(const std::set<std::string> & files)1537 void ImpBase::set_monitor_files(const std::set<std::string> &files)
1538 {
1539 for (const auto &filename : files) {
1540 if (file_monitors.count(filename) == 0) {
1541 auto mon = Gio::File::create_for_path(filename)->monitor_file();
1542 mon->signal_changed().connect(sigc::mem_fun(*this, &ImpBase::handle_file_changed));
1543 file_monitors[filename] = mon;
1544 }
1545 }
1546 map_erase_if(file_monitors, [files](auto &it) { return files.count(it.first) == 0; });
1547 }
1548
set_monitor_items(const ItemSet & items)1549 void ImpBase::set_monitor_items(const ItemSet &items)
1550 {
1551 std::set<std::string> filenames;
1552 std::transform(items.begin(), items.end(), std::inserter(filenames, filenames.begin()),
1553 [this](const auto &it) { return pool->get_filename(it.first, it.second); });
1554 file_monitor_delay_connection.disconnect();
1555 file_monitor_delay_connection = Glib::signal_timeout().connect_seconds(
1556 [this, filenames] {
1557 set_monitor_files(filenames);
1558 return false;
1559 },
1560 1);
1561 }
1562
handle_file_changed(const Glib::RefPtr<Gio::File> & file1,const Glib::RefPtr<Gio::File> & file2,Gio::FileMonitorEvent ev)1563 void ImpBase::handle_file_changed(const Glib::RefPtr<Gio::File> &file1, const Glib::RefPtr<Gio::File> &file2,
1564 Gio::FileMonitorEvent ev)
1565 {
1566 main_window->show_nonmodal(
1567 "Pool has changed", "Reload pool", [this] { trigger_action(ActionID::RELOAD_POOL); },
1568 "This will clear the undo/redo history");
1569 }
1570
set_read_only(bool v)1571 void ImpBase::set_read_only(bool v)
1572 {
1573 read_only = v;
1574 }
1575
tool_update_data(std::unique_ptr<ToolData> & data)1576 void ImpBase::tool_update_data(std::unique_ptr<ToolData> &data)
1577 {
1578
1579 if (core->tool_is_active()) {
1580 ToolArgs args;
1581 args.type = ToolEventType::DATA;
1582 args.data = std::move(data);
1583 args.coords = canvas->get_cursor_pos();
1584 ToolResponse r = core->tool_update(args);
1585 tool_process(r);
1586 }
1587 }
1588
1589
get_doubleclick_action(ObjectType type,const UUID & uu)1590 ActionToolID ImpBase::get_doubleclick_action(ObjectType type, const UUID &uu)
1591 {
1592 switch (type) {
1593 case ObjectType::TEXT:
1594 return make_action(ToolID::ENTER_DATUM);
1595 break;
1596 case ObjectType::POLYGON_ARC_CENTER:
1597 case ObjectType::POLYGON_VERTEX:
1598 case ObjectType::POLYGON_EDGE:
1599 return make_action(ActionID::SELECT_POLYGON);
1600 break;
1601 default:
1602 return {ActionID::NONE, ToolID::NONE};
1603 }
1604 }
1605
get_selection_filter_info() const1606 std::map<ObjectType, ImpBase::SelectionFilterInfo> ImpBase::get_selection_filter_info() const
1607 {
1608 std::map<ObjectType, ImpBase::SelectionFilterInfo> r;
1609 for (const auto &it : object_descriptions) {
1610 ObjectType type = it.first;
1611 if (type == ObjectType::POLYGON_ARC_CENTER || type == ObjectType::POLYGON_EDGE
1612 || type == ObjectType::POLYGON_VERTEX)
1613 type = ObjectType::POLYGON;
1614 if (core->has_object_type(type)) {
1615 r[type];
1616 }
1617 }
1618 return r;
1619 }
1620
1621 const std::string ImpBase::meta_suffix = ".imp_meta";
1622
load_meta()1623 void ImpBase::load_meta()
1624 {
1625 std::string meta_filename = core->get_filename() + meta_suffix;
1626 if (Glib::file_test(meta_filename, Glib::FILE_TEST_IS_REGULAR)) {
1627 m_meta = load_json_from_file(meta_filename);
1628 }
1629 else {
1630 m_meta = core->get_meta();
1631 }
1632 }
1633
set_window_title(const std::string & s)1634 void ImpBase::set_window_title(const std::string &s)
1635 {
1636 if (s.size()) {
1637 main_window->set_title(s + " - " + object_descriptions.at(get_editor_type()).name);
1638 }
1639 else {
1640 main_window->set_title("Untitled " + object_descriptions.at(get_editor_type()).name);
1641 }
1642 }
1643
1644
set_window_title_from_block()1645 void ImpBase::set_window_title_from_block()
1646 {
1647 std::string title;
1648 if (core->get_block()->project_meta.count("project_title"))
1649 title = core->get_block()->project_meta.at("project_title");
1650
1651 set_window_title(title);
1652 }
1653
get_view_hints()1654 std::vector<std::string> ImpBase::get_view_hints()
1655 {
1656 std::vector<std::string> r;
1657 if (distraction_free)
1658 r.emplace_back("distraction free mode");
1659
1660 if (selection_filter_dialog->get_filtered())
1661 r.emplace_back("selection filtered");
1662 return r;
1663 }
1664
update_view_hints()1665 void ImpBase::update_view_hints()
1666 {
1667 auto hints = get_view_hints();
1668 main_window->set_view_hints_label(hints);
1669 }
1670
is_poly(ObjectType type)1671 static bool is_poly(ObjectType type)
1672 {
1673 switch (type) {
1674 case ObjectType::POLYGON_ARC_CENTER:
1675 case ObjectType::POLYGON_EDGE:
1676 case ObjectType::POLYGON_VERTEX:
1677 return true;
1678 default:
1679 return false;
1680 }
1681 }
1682
handle_select_polygon(const ActionConnection & a)1683 void ImpBase::handle_select_polygon(const ActionConnection &a)
1684 {
1685 auto sel = canvas->get_selection();
1686 auto new_sel = sel;
1687 for (const auto &it : sel) {
1688 if (is_poly(it.type)) {
1689 auto poly = core->get_polygon(it.uuid);
1690 unsigned int v = 0;
1691 for (const auto &it_v : poly->vertices) {
1692 new_sel.emplace(it.uuid, ObjectType::POLYGON_VERTEX, v);
1693 new_sel.emplace(it.uuid, ObjectType::POLYGON_EDGE, v);
1694 if (it_v.type == Polygon::Vertex::Type::ARC)
1695 new_sel.emplace(it.uuid, ObjectType::POLYGON_ARC_CENTER, v);
1696 v++;
1697 }
1698 }
1699 }
1700 canvas->set_selection(new_sel, false);
1701 canvas->set_selection_mode(CanvasGL::SelectionMode::NORMAL);
1702 }
1703
get_editor_type() const1704 ObjectType ImpBase::get_editor_type() const
1705 {
1706 return core->get_object_type();
1707 }
1708
check_version()1709 void ImpBase::check_version()
1710 {
1711 const auto &version = core->get_version();
1712 main_window->set_version_info(version.get_message(core->get_object_type()));
1713 if (version.get_app() < version.get_file()) {
1714 set_read_only(true);
1715 }
1716 }
1717
view_options_menu_append_action(const std::string & label,const std::string & action)1718 void ImpBase::view_options_menu_append_action(const std::string &label, const std::string &action)
1719 {
1720 auto bu = Gtk::manage(new Gtk::ModelButton);
1721 bu->set_label(label);
1722 bu->get_child()->set_halign(Gtk::ALIGN_START);
1723 gtk_actionable_set_action_name(GTK_ACTIONABLE(bu->gobj()), action.c_str());
1724 view_options_menu->pack_start(*bu, false, false, 0);
1725 bu->show();
1726 }
1727
get_cursor_icon(ToolID id)1728 static const char *get_cursor_icon(ToolID id)
1729 {
1730 switch (id) {
1731 case ToolID::PLACE_TEXT:
1732 case ToolID::PLACE_REFDES_AND_VALUE:
1733 case ToolID::PLACE_SHAPE:
1734 case ToolID::PLACE_SHAPE_OBROUND:
1735 case ToolID::PLACE_SHAPE_RECTANGLE:
1736 case ToolID::PLACE_HOLE:
1737 case ToolID::PLACE_HOLE_SLOT:
1738 case ToolID::PLACE_BOARD_HOLE:
1739 case ToolID::PLACE_PAD:
1740 case ToolID::PLACE_POWER_SYMBOL:
1741 case ToolID::PLACE_NET_LABEL:
1742 case ToolID::PLACE_BUS_LABEL:
1743 case ToolID::PLACE_BUS_RIPPER:
1744 case ToolID::PLACE_VIA:
1745 case ToolID::PLACE_DOT:
1746 return nullptr;
1747
1748 default:
1749 return get_action_icon(make_action(id));
1750 }
1751 }
1752
update_cursor(ToolID tool_id)1753 void ImpBase::update_cursor(ToolID tool_id)
1754 {
1755 auto win = canvas->get_window();
1756 if (tool_id == ToolID::NONE) {
1757 win->set_cursor();
1758 return;
1759 }
1760 static const int icon_size = 16;
1761 static const int border_width = 1;
1762 auto surf = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, icon_size + border_width * 2,
1763 icon_size + border_width * 2);
1764 auto cr = Cairo::Context::create(surf);
1765 auto theme = Gtk::IconTheme::get_default();
1766 const auto icon_name = get_cursor_icon(tool_id);
1767 if (!icon_name) {
1768 win->set_cursor();
1769 return;
1770 }
1771 auto icon_info = theme->lookup_icon(icon_name, icon_size);
1772 if (!icon_info) {
1773 win->set_cursor();
1774 return;
1775 }
1776 bool was_symbolic = false;
1777 auto pb_black = icon_info.load_symbolic(Gdk::RGBA("#000000"), Gdk::RGBA(), Gdk::RGBA(), Gdk::RGBA(), was_symbolic);
1778 Gdk::Cairo::set_source_pixbuf(cr, pb_black, border_width, border_width);
1779 auto pat = cr->get_source();
1780 for (int x : {-1, 1, 0}) {
1781 for (int y : {-1, 1, 0}) {
1782 cr->save();
1783 cr->translate(x * border_width, y * border_width);
1784 cr->set_source_rgb(1, 1, 1);
1785 cr->mask(pat);
1786 cr->restore();
1787 }
1788 }
1789 Gdk::Cairo::set_source_pixbuf(cr, pb_black, border_width, border_width);
1790 cr->paint();
1791 win->set_cursor(Gdk::Cursor::create(win->get_display(), surf, 0, 0));
1792 }
1793 } // namespace horizon
1794