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 &parametric;
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 &params)
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