1 //-----------------------------------------------------------------------------
2 // Our main() function, and GTK3-specific stuff to set up our windows and
3 // otherwise handle our interface to the operating system. Everything
4 // outside gtk/... should be standard C++ and OpenGL.
5 //
6 // Copyright 2015 <whitequark@whitequark.org>
7 //-----------------------------------------------------------------------------
8 #include <errno.h>
9 #include <sys/stat.h>
10 #include <unistd.h>
11 #include <time.h>
12 
13 #include <iostream>
14 
15 #include <json-c/json_object.h>
16 #include <json-c/json_util.h>
17 
18 #include <glibmm/main.h>
19 #include <glibmm/convert.h>
20 #include <giomm/file.h>
21 #include <gdkmm/cursor.h>
22 #include <gtkmm/drawingarea.h>
23 #include <gtkmm/scrollbar.h>
24 #include <gtkmm/entry.h>
25 #include <gtkmm/eventbox.h>
26 #include <gtkmm/fixed.h>
27 #include <gtkmm/adjustment.h>
28 #include <gtkmm/separatormenuitem.h>
29 #include <gtkmm/menuitem.h>
30 #include <gtkmm/checkmenuitem.h>
31 #include <gtkmm/radiomenuitem.h>
32 #include <gtkmm/radiobuttongroup.h>
33 #include <gtkmm/menu.h>
34 #include <gtkmm/menubar.h>
35 #include <gtkmm/scrolledwindow.h>
36 #include <gtkmm/filechooserdialog.h>
37 #include <gtkmm/messagedialog.h>
38 #include <gtkmm/main.h>
39 
40 #if HAVE_GTK3
41 #include <gtkmm/hvbox.h>
42 #else
43 #include <gtkmm/box.h>
44 #endif
45 
46 #include <cairomm/xlib_surface.h>
47 #include <pangomm/fontdescription.h>
48 #include <gdk/gdkx.h>
49 #include <fontconfig/fontconfig.h>
50 
51 #include <GL/glx.h>
52 
53 #include "solvespace.h"
54 #include "config.h"
55 #include "../unix/gloffscreen.h"
56 
57 #ifdef HAVE_SPACEWARE
58 #include <spnav.h>
59 #endif
60 
61 namespace SolveSpace {
62 /* Settings */
63 
64 /* Why not just use GSettings? Two reasons. It doesn't allow to easily see
65    whether the setting had the default value, and it requires to install
66    a schema globally. */
67 static json_object *settings = NULL;
68 
CnfPrepare()69 static std::string CnfPrepare() {
70     // Refer to http://standards.freedesktop.org/basedir-spec/latest/
71 
72     std::string dir;
73     if(getenv("XDG_CONFIG_HOME")) {
74         dir = std::string(getenv("XDG_CONFIG_HOME")) + "/solvespace";
75     } else if(getenv("HOME")) {
76         dir = std::string(getenv("HOME")) + "/.config/solvespace";
77     } else {
78         dbp("neither XDG_CONFIG_HOME nor HOME are set");
79         return "";
80     }
81 
82     struct stat st;
83     if(stat(dir.c_str(), &st)) {
84         if(errno == ENOENT) {
85             if(mkdir(dir.c_str(), 0777)) {
86                 dbp("cannot mkdir %s: %s", dir.c_str(), strerror(errno));
87                 return "";
88             }
89         } else {
90             dbp("cannot stat %s: %s", dir.c_str(), strerror(errno));
91             return "";
92         }
93     } else if(!S_ISDIR(st.st_mode)) {
94         dbp("%s is not a directory", dir.c_str());
95         return "";
96     }
97 
98     return dir + "/settings.json";
99 }
100 
CnfLoad()101 static void CnfLoad() {
102     std::string path = CnfPrepare();
103     if(path.empty())
104         return;
105 
106     if(settings)
107         json_object_put(settings); // deallocate
108 
109     settings = json_object_from_file(path.c_str());
110     if(!settings) {
111         if(errno != ENOENT)
112             dbp("cannot load settings: %s", strerror(errno));
113 
114         settings = json_object_new_object();
115     }
116 }
117 
CnfSave()118 static void CnfSave() {
119     std::string path = CnfPrepare();
120     if(path.empty())
121         return;
122 
123     /* json-c <0.12 has the first argument non-const here */
124     if(json_object_to_file_ext((char*) path.c_str(), settings, JSON_C_TO_STRING_PRETTY))
125         dbp("cannot save settings: %s", strerror(errno));
126 }
127 
CnfFreezeInt(uint32_t val,const std::string & key)128 void CnfFreezeInt(uint32_t val, const std::string &key) {
129     struct json_object *jval = json_object_new_int(val);
130     json_object_object_add(settings, key.c_str(), jval);
131     CnfSave();
132 }
133 
CnfThawInt(uint32_t val,const std::string & key)134 uint32_t CnfThawInt(uint32_t val, const std::string &key) {
135     struct json_object *jval;
136     if(json_object_object_get_ex(settings, key.c_str(), &jval))
137         return json_object_get_int(jval);
138     else return val;
139 }
140 
CnfFreezeFloat(float val,const std::string & key)141 void CnfFreezeFloat(float val, const std::string &key) {
142     struct json_object *jval = json_object_new_double(val);
143     json_object_object_add(settings, key.c_str(), jval);
144     CnfSave();
145 }
146 
CnfThawFloat(float val,const std::string & key)147 float CnfThawFloat(float val, const std::string &key) {
148     struct json_object *jval;
149     if(json_object_object_get_ex(settings, key.c_str(), &jval))
150         return json_object_get_double(jval);
151     else return val;
152 }
153 
CnfFreezeString(const std::string & val,const std::string & key)154 void CnfFreezeString(const std::string &val, const std::string &key) {
155     struct json_object *jval = json_object_new_string(val.c_str());
156     json_object_object_add(settings, key.c_str(), jval);
157     CnfSave();
158 }
159 
CnfThawString(const std::string & val,const std::string & key)160 std::string CnfThawString(const std::string &val, const std::string &key) {
161     struct json_object *jval;
162     if(json_object_object_get_ex(settings, key.c_str(), &jval))
163         return json_object_get_string(jval);
164     return val;
165 }
166 
CnfFreezeWindowPos(Gtk::Window * win,const std::string & key)167 static void CnfFreezeWindowPos(Gtk::Window *win, const std::string &key) {
168     int x, y, w, h;
169     win->get_position(x, y);
170     win->get_size(w, h);
171 
172     CnfFreezeInt(x, key + "_left");
173     CnfFreezeInt(y, key + "_top");
174     CnfFreezeInt(w, key + "_width");
175     CnfFreezeInt(h, key + "_height");
176 }
177 
CnfThawWindowPos(Gtk::Window * win,const std::string & key)178 static void CnfThawWindowPos(Gtk::Window *win, const std::string &key) {
179     int x, y, w, h;
180     win->get_position(x, y);
181     win->get_size(w, h);
182 
183     x = CnfThawInt(x, key + "_left");
184     y = CnfThawInt(y, key + "_top");
185     w = CnfThawInt(w, key + "_width");
186     h = CnfThawInt(h, key + "_height");
187 
188     win->move(x, y);
189     win->resize(w, h);
190 }
191 
192 /* Timers */
193 
GetMilliseconds(void)194 int64_t GetMilliseconds(void) {
195     struct timespec ts;
196     clock_gettime(CLOCK_MONOTONIC, &ts);
197     return 1000 * (uint64_t) ts.tv_sec + ts.tv_nsec / 1000000;
198 }
199 
TimerCallback()200 static bool TimerCallback() {
201     SS.GW.TimerCallback();
202     SS.TW.TimerCallback();
203     return false;
204 }
205 
SetTimerFor(int milliseconds)206 void SetTimerFor(int milliseconds) {
207     Glib::signal_timeout().connect(&TimerCallback, milliseconds);
208 }
209 
AutosaveTimerCallback()210 static bool AutosaveTimerCallback() {
211     SS.Autosave();
212     return false;
213 }
214 
SetAutosaveTimerFor(int minutes)215 void SetAutosaveTimerFor(int minutes) {
216     Glib::signal_timeout().connect(&AutosaveTimerCallback, minutes * 60 * 1000);
217 }
218 
LaterCallback()219 static bool LaterCallback() {
220     SS.DoLater();
221     return false;
222 }
223 
ScheduleLater()224 void ScheduleLater() {
225     Glib::signal_idle().connect(&LaterCallback);
226 }
227 
228 /* GL wrapper */
229 
230 #define GL_CHECK() \
231     do { \
232         int err = (int)glGetError(); \
233         if(err) dbp("%s:%d: glGetError() == 0x%X %s", \
234                     __FILE__, __LINE__, err, gluErrorString(err)); \
235     } while (0)
236 
237 class GlWidget : public Gtk::DrawingArea {
238 public:
GlWidget()239     GlWidget() : _offscreen(NULL) {
240         _xdisplay = gdk_x11_get_default_xdisplay();
241 
242         int glxmajor, glxminor;
243         if(!glXQueryVersion(_xdisplay, &glxmajor, &glxminor)) {
244             dbp("OpenGL is not supported");
245             oops();
246         }
247 
248         if(glxmajor < 1 || (glxmajor == 1 && glxminor < 3)) {
249             dbp("GLX version %d.%d is too old; 1.3 required", glxmajor, glxminor);
250             oops();
251         }
252 
253         static int fbconfig_attrs[] = {
254             GLX_RENDER_TYPE, GLX_RGBA_BIT,
255             GLX_RED_SIZE, 8,
256             GLX_GREEN_SIZE, 8,
257             GLX_BLUE_SIZE, 8,
258             GLX_DEPTH_SIZE, 24,
259             None
260         };
261         int fbconfig_num = 0;
262         GLXFBConfig *fbconfigs = glXChooseFBConfig(_xdisplay, DefaultScreen(_xdisplay),
263                 fbconfig_attrs, &fbconfig_num);
264         if(!fbconfigs || fbconfig_num == 0)
265             oops();
266 
267         /* prefer FBConfigs with depth of 32;
268             * Mesa software rasterizer explodes with a BadMatch without this;
269             * without this, Intel on Mesa flickers horribly for some reason.
270            this does not seem to affect other rasterizers (ie NVidia).
271 
272            see this Mesa bug:
273            http://lists.freedesktop.org/archives/mesa-dev/2015-January/074693.html */
274         GLXFBConfig fbconfig = fbconfigs[0];
275         for(int i = 0; i < fbconfig_num; i++) {
276             XVisualInfo *visual_info = glXGetVisualFromFBConfig(_xdisplay, fbconfigs[i]);
277             /* some GL visuals, notably on Chromium GL, do not have an associated
278                X visual; this is not an obstacle as we always render offscreen. */
279             if(!visual_info) continue;
280             int depth = visual_info->depth;
281             XFree(visual_info);
282 
283             if(depth == 32) {
284                 fbconfig = fbconfigs[i];
285                 break;
286             }
287         }
288 
289         _glcontext = glXCreateNewContext(_xdisplay,
290                 fbconfig, GLX_RGBA_TYPE, 0, True);
291         if(!_glcontext) {
292             dbp("cannot create OpenGL context");
293             oops();
294         }
295 
296         XFree(fbconfigs);
297 
298         /* create a dummy X window to create a rendering context against.
299            we could use a Pbuffer, but some implementations (Chromium GL)
300            don't support these. we could use an existing window, but
301            some implementations (Chromium GL... do you see a pattern?)
302            do really strange things, i.e. draw a black rectangle on
303            the very front of the desktop if you do this. */
304         _xwindow = XCreateSimpleWindow(_xdisplay,
305                 XRootWindow(_xdisplay, gdk_x11_get_default_screen()),
306                 /*x*/ 0, /*y*/ 0, /*width*/ 1, /*height*/ 1,
307                 /*border_width*/ 0, /*border*/ 0, /*background*/ 0);
308     }
309 
~GlWidget()310     ~GlWidget() {
311         glXMakeCurrent(_xdisplay, None, NULL);
312 
313         XDestroyWindow(_xdisplay, _xwindow);
314 
315         delete _offscreen;
316 
317         glXDestroyContext(_xdisplay, _glcontext);
318     }
319 
320 protected:
321     /* Draw on a GLX framebuffer object, then read pixels out and draw them on
322        the Cairo context. Slower, but you get to overlay nice widgets. */
on_draw(const Cairo::RefPtr<Cairo::Context> & cr)323     virtual bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
324         if(!glXMakeCurrent(_xdisplay, _xwindow, _glcontext))
325             oops();
326 
327         if(!_offscreen)
328             _offscreen = new GLOffscreen;
329 
330         Gdk::Rectangle allocation = get_allocation();
331         if(!_offscreen->begin(allocation.get_width(), allocation.get_height()))
332             oops();
333 
334         on_gl_draw();
335         glFlush();
336         GL_CHECK();
337 
338         Cairo::RefPtr<Cairo::ImageSurface> surface = Cairo::ImageSurface::create(
339                 _offscreen->end(), Cairo::FORMAT_RGB24,
340                 allocation.get_width(), allocation.get_height(), allocation.get_width() * 4);
341         cr->set_source(surface, 0, 0);
342         cr->paint();
343         surface->finish();
344 
345         return true;
346     }
347 
348 #ifdef HAVE_GTK2
on_expose_event(GdkEventExpose *)349     virtual bool on_expose_event(GdkEventExpose *) {
350         return on_draw(get_window()->create_cairo_context());
351     }
352 #endif
353 
354     virtual void on_gl_draw() = 0;
355 
356 private:
357     Display *_xdisplay;
358     GLXContext _glcontext;
359     GLOffscreen *_offscreen;
360     ::Window _xwindow;
361 };
362 
363 /* Editor overlay */
364 
365 class EditorOverlay : public Gtk::Fixed {
366 public:
EditorOverlay(Gtk::Widget & underlay)367     EditorOverlay(Gtk::Widget &underlay) : _underlay(underlay) {
368         set_size_request(0, 0);
369 
370         add(_underlay);
371 
372         _entry.set_no_show_all(true);
373         _entry.set_has_frame(false);
374         add(_entry);
375 
376         _entry.signal_activate().
377             connect(sigc::mem_fun(this, &EditorOverlay::on_activate));
378     }
379 
start_editing(int x,int y,int font_height,bool is_monospace,int minWidthChars,const std::string & val)380     void start_editing(int x, int y, int font_height, bool is_monospace, int minWidthChars,
381                        const std::string &val) {
382         Pango::FontDescription font_desc;
383         font_desc.set_family(is_monospace ? "monospace" : "normal");
384         font_desc.set_absolute_size(font_height * Pango::SCALE);
385 
386 #ifdef HAVE_GTK3
387         /* For some reason override_font doesn't take screen DPI into
388            account on GTK3 when working with font descriptors specified
389            in absolute sizes; modify_font does on GTK2. */
390         Pango::FontDescription override_font_desc(font_desc);
391         double dpi = get_screen()->get_resolution();
392         override_font_desc.set_size(font_height * 72.0 / dpi * Pango::SCALE);
393         _entry.override_font(override_font_desc);
394 #else
395         _entry.modify_font(font_desc);
396 #endif
397 
398         /* y coordinate denotes baseline */
399         Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc);
400         y -= font_metrics.get_ascent() / Pango::SCALE;
401 
402         Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
403         layout->set_font_description(font_desc);
404         layout->set_text(val + " "); /* avoid scrolling */
405         int width = layout->get_logical_extents().get_width();
406 
407 #ifdef HAVE_GTK3
408         Gtk::Border border = _entry.get_style_context()->get_padding();
409         move(_entry, x - border.get_left(), y - border.get_top());
410         _entry.set_width_chars(minWidthChars);
411         _entry.set_size_request(width / Pango::SCALE, -1);
412 #else
413         /* We need _gtk_entry_effective_inner_border, but it's not
414            in the public API, so emulate its logic. */
415         Gtk::Border border = { 2, 2, 2, 2 }, *style_border;
416         gtk_widget_style_get(GTK_WIDGET(_entry.gobj()), "inner-border",
417                              &style_border, NULL);
418         if(style_border) border = *style_border;
419         move(_entry, x - border.left, y - border.top);
420         /* This is what set_width_chars does. */
421         int minWidth = minWidthChars * std::max(font_metrics.get_approximate_digit_width(),
422                                                 font_metrics.get_approximate_char_width());
423         _entry.set_size_request(std::max(width, minWidth) / Pango::SCALE, -1);
424 #endif
425 
426         _entry.set_text(val);
427         if(!_entry.is_visible()) {
428             _entry.show();
429             _entry.grab_focus();
430             _entry.add_modal_grab();
431         }
432     }
433 
stop_editing()434     void stop_editing() {
435         if(_entry.is_visible())
436             _entry.remove_modal_grab();
437         _entry.hide();
438     }
439 
is_editing() const440     bool is_editing() const {
441         return _entry.is_visible();
442     }
443 
signal_editing_done()444     sigc::signal<void, Glib::ustring> signal_editing_done() {
445         return _signal_editing_done;
446     }
447 
get_entry()448     Gtk::Entry &get_entry() {
449         return _entry;
450     }
451 
452 protected:
on_key_press_event(GdkEventKey * event)453     virtual bool on_key_press_event(GdkEventKey *event) {
454         if(event->keyval == GDK_KEY_Escape) {
455             stop_editing();
456             return true;
457         }
458 
459         return false;
460     }
461 
on_size_allocate(Gtk::Allocation & allocation)462     virtual void on_size_allocate(Gtk::Allocation& allocation) {
463         Gtk::Fixed::on_size_allocate(allocation);
464 
465         _underlay.size_allocate(allocation);
466     }
467 
on_activate()468     virtual void on_activate() {
469         _signal_editing_done(_entry.get_text());
470     }
471 
472 private:
473     Gtk::Widget &_underlay;
474     Gtk::Entry _entry;
475     sigc::signal<void, Glib::ustring> _signal_editing_done;
476 };
477 
478 /* Graphics window */
479 
DeltaYOfScrollEvent(GdkEventScroll * event)480 int DeltaYOfScrollEvent(GdkEventScroll *event) {
481 #ifdef HAVE_GTK3
482     int delta_y = event->delta_y;
483 #else
484     int delta_y = 0;
485 #endif
486     if(delta_y == 0) {
487         switch(event->direction) {
488             case GDK_SCROLL_UP:
489             delta_y = -1;
490             break;
491 
492             case GDK_SCROLL_DOWN:
493             delta_y = 1;
494             break;
495 
496             default:
497             /* do nothing */
498             return false;
499         }
500     }
501 
502     return delta_y;
503 }
504 
505 class GraphicsWidget : public GlWidget {
506 public:
GraphicsWidget()507     GraphicsWidget() {
508         set_events(Gdk::POINTER_MOTION_MASK |
509                    Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK |
510                    Gdk::SCROLL_MASK |
511                    Gdk::LEAVE_NOTIFY_MASK);
512         set_double_buffered(true);
513     }
514 
515 protected:
on_configure_event(GdkEventConfigure * event)516     virtual bool on_configure_event(GdkEventConfigure *event) {
517         _w = event->width;
518         _h = event->height;
519 
520         return GlWidget::on_configure_event(event);;
521     }
522 
on_gl_draw()523     virtual void on_gl_draw() {
524         SS.GW.Paint();
525     }
526 
on_motion_notify_event(GdkEventMotion * event)527     virtual bool on_motion_notify_event(GdkEventMotion *event) {
528         int x, y;
529         ij_to_xy(event->x, event->y, x, y);
530 
531         SS.GW.MouseMoved(x, y,
532             event->state & GDK_BUTTON1_MASK,
533             event->state & GDK_BUTTON2_MASK,
534             event->state & GDK_BUTTON3_MASK,
535             event->state & GDK_SHIFT_MASK,
536             event->state & GDK_CONTROL_MASK);
537 
538         return true;
539     }
540 
on_button_press_event(GdkEventButton * event)541     virtual bool on_button_press_event(GdkEventButton *event) {
542         int x, y;
543         ij_to_xy(event->x, event->y, x, y);
544 
545         switch(event->button) {
546             case 1:
547             if(event->type == GDK_BUTTON_PRESS)
548                 SS.GW.MouseLeftDown(x, y);
549             else if(event->type == GDK_2BUTTON_PRESS)
550                 SS.GW.MouseLeftDoubleClick(x, y);
551             break;
552 
553             case 2:
554             case 3:
555             SS.GW.MouseMiddleOrRightDown(x, y);
556             break;
557         }
558 
559         return true;
560     }
561 
on_button_release_event(GdkEventButton * event)562     virtual bool on_button_release_event(GdkEventButton *event) {
563         int x, y;
564         ij_to_xy(event->x, event->y, x, y);
565 
566         switch(event->button) {
567             case 1:
568             SS.GW.MouseLeftUp(x, y);
569             break;
570 
571             case 3:
572             SS.GW.MouseRightUp(x, y);
573             break;
574         }
575 
576         return true;
577     }
578 
on_scroll_event(GdkEventScroll * event)579     virtual bool on_scroll_event(GdkEventScroll *event) {
580         int x, y;
581         ij_to_xy(event->x, event->y, x, y);
582 
583         SS.GW.MouseScroll(x, y, -DeltaYOfScrollEvent(event));
584 
585         return true;
586     }
587 
on_leave_notify_event(GdkEventCrossing *)588     virtual bool on_leave_notify_event (GdkEventCrossing *) {
589         SS.GW.MouseLeave();
590 
591         return true;
592     }
593 
594 private:
595     int _w, _h;
ij_to_xy(int i,int j,int & x,int & y)596     void ij_to_xy(int i, int j, int &x, int &y) {
597         // Convert to xy (vs. ij) style coordinates,
598         // with (0, 0) at center
599         x = i - _w / 2;
600         y = _h / 2 - j;
601     }
602 };
603 
604 class GraphicsWindowGtk : public Gtk::Window {
605 public:
GraphicsWindowGtk()606     GraphicsWindowGtk() : _overlay(_widget), _is_fullscreen(false) {
607         set_default_size(900, 600);
608 
609         _box.pack_start(_menubar, false, true);
610         _box.pack_start(_overlay, true, true);
611 
612         add(_box);
613 
614         _overlay.signal_editing_done().
615             connect(sigc::mem_fun(this, &GraphicsWindowGtk::on_editing_done));
616     }
617 
get_widget()618     GraphicsWidget &get_widget() {
619         return _widget;
620     }
621 
get_overlay()622     EditorOverlay &get_overlay() {
623         return _overlay;
624     }
625 
get_menubar()626     Gtk::MenuBar &get_menubar() {
627         return _menubar;
628     }
629 
is_fullscreen() const630     bool is_fullscreen() const {
631         return _is_fullscreen;
632     }
633 
emulate_key_press(GdkEventKey * event)634     bool emulate_key_press(GdkEventKey *event) {
635         return on_key_press_event(event);
636     }
637 
638 protected:
on_show()639     virtual void on_show() {
640         Gtk::Window::on_show();
641 
642         CnfThawWindowPos(this, "GraphicsWindow");
643     }
644 
on_hide()645     virtual void on_hide() {
646         CnfFreezeWindowPos(this, "GraphicsWindow");
647 
648         Gtk::Window::on_hide();
649     }
650 
on_delete_event(GdkEventAny *)651     virtual bool on_delete_event(GdkEventAny *) {
652         if(!SS.OkayToStartNewFile()) return true;
653         SS.Exit();
654 
655         return true;
656     }
657 
on_window_state_event(GdkEventWindowState * event)658     virtual bool on_window_state_event(GdkEventWindowState *event) {
659         _is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
660 
661         /* The event arrives too late for the caller of ToggleFullScreen
662            to notice state change; and it's possible that the WM will
663            refuse our request, so we can't just toggle the saved state */
664         SS.GW.EnsureValidActives();
665 
666         return Gtk::Window::on_window_state_event(event);
667     }
668 
on_key_press_event(GdkEventKey * event)669     virtual bool on_key_press_event(GdkEventKey *event) {
670         int chr;
671 
672         switch(event->keyval) {
673             case GDK_KEY_Escape:
674             chr = GraphicsWindow::ESCAPE_KEY;
675             break;
676 
677             case GDK_KEY_Delete:
678             chr = GraphicsWindow::DELETE_KEY;
679             break;
680 
681             case GDK_KEY_Tab:
682             chr = '\t';
683             break;
684 
685             case GDK_KEY_BackSpace:
686             case GDK_KEY_Back:
687             chr = '\b';
688             break;
689 
690             default:
691             if(event->keyval >= GDK_KEY_F1 && event->keyval <= GDK_KEY_F12) {
692                 chr = GraphicsWindow::FUNCTION_KEY_BASE + (event->keyval - GDK_KEY_F1);
693             } else {
694                 chr = gdk_keyval_to_unicode(event->keyval);
695             }
696         }
697 
698         if(event->state & GDK_SHIFT_MASK){
699             chr |= GraphicsWindow::SHIFT_MASK;
700         }
701         if(event->state & GDK_CONTROL_MASK) {
702             chr |= GraphicsWindow::CTRL_MASK;
703         }
704 
705         if(chr && SS.GW.KeyDown(chr)) {
706             return true;
707         }
708 
709         if(chr == '\t') {
710             // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=123994.
711             GraphicsWindow::MenuView(GraphicsWindow::MNU_SHOW_TEXT_WND);
712             return true;
713         }
714 
715         return Gtk::Window::on_key_press_event(event);
716     }
717 
on_editing_done(Glib::ustring value)718     virtual void on_editing_done(Glib::ustring value) {
719         SS.GW.EditControlDone(value.c_str());
720     }
721 
722 private:
723     GraphicsWidget _widget;
724     EditorOverlay _overlay;
725     Gtk::MenuBar _menubar;
726     Gtk::VBox _box;
727 
728     bool _is_fullscreen;
729 };
730 
731 std::unique_ptr<GraphicsWindowGtk> GW;
732 
GetGraphicsWindowSize(int * w,int * h)733 void GetGraphicsWindowSize(int *w, int *h) {
734     Gdk::Rectangle allocation = GW->get_widget().get_allocation();
735     *w = allocation.get_width();
736     *h = allocation.get_height();
737 }
738 
InvalidateGraphics(void)739 void InvalidateGraphics(void) {
740     GW->get_widget().queue_draw();
741 }
742 
PaintGraphics(void)743 void PaintGraphics(void) {
744     GW->get_widget().queue_draw();
745     /* Process animation */
746     Glib::MainContext::get_default()->iteration(false);
747 }
748 
SetCurrentFilename(const std::string & filename)749 void SetCurrentFilename(const std::string &filename) {
750     if(!filename.empty()) {
751         GW->set_title("SolveSpace - " + filename);
752     } else {
753         GW->set_title("SolveSpace - (not yet saved)");
754     }
755 }
756 
ToggleFullScreen(void)757 void ToggleFullScreen(void) {
758     if(GW->is_fullscreen())
759         GW->unfullscreen();
760     else
761         GW->fullscreen();
762 }
763 
FullScreenIsActive(void)764 bool FullScreenIsActive(void) {
765     return GW->is_fullscreen();
766 }
767 
ShowGraphicsEditControl(int x,int y,int fontHeight,int minWidthChars,const std::string & val)768 void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars,
769                              const std::string &val) {
770     Gdk::Rectangle rect = GW->get_widget().get_allocation();
771 
772     // Convert to ij (vs. xy) style coordinates,
773     // and compensate for the input widget height due to inverse coord
774     int i, j;
775     i = x + rect.get_width() / 2;
776     j = -y + rect.get_height() / 2;
777 
778     GW->get_overlay().start_editing(i, j, fontHeight, /*is_monospace=*/false, minWidthChars, val);
779 }
780 
HideGraphicsEditControl(void)781 void HideGraphicsEditControl(void) {
782     GW->get_overlay().stop_editing();
783 }
784 
GraphicsEditControlIsVisible(void)785 bool GraphicsEditControlIsVisible(void) {
786     return GW->get_overlay().is_editing();
787 }
788 
789 /* TODO: removing menubar breaks accelerators. */
ToggleMenuBar(void)790 void ToggleMenuBar(void) {
791     GW->get_menubar().set_visible(!GW->get_menubar().is_visible());
792 }
793 
MenuBarIsVisible(void)794 bool MenuBarIsVisible(void) {
795     return GW->get_menubar().is_visible();
796 }
797 
798 /* Context menus */
799 
800 class ContextMenuItem : public Gtk::MenuItem {
801 public:
802     static int choice;
803 
ContextMenuItem(const Glib::ustring & label,int id,bool mnemonic=false)804     ContextMenuItem(const Glib::ustring &label, int id, bool mnemonic=false) :
805             Gtk::MenuItem(label, mnemonic), _id(id) {
806     }
807 
808 protected:
on_activate()809     virtual void on_activate() {
810         Gtk::MenuItem::on_activate();
811 
812         if(has_submenu())
813             return;
814 
815         choice = _id;
816     }
817 
818     /* Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=695488.
819        This is used in addition to on_activate() to catch mouse events.
820        Without on_activate(), it would be impossible to select a menu item
821        via keyboard.
822        This selects the item twice in some cases, but we are idempotent.
823      */
on_button_press_event(GdkEventButton * event)824     virtual bool on_button_press_event(GdkEventButton *event) {
825         if(event->button == 1 && event->type == GDK_BUTTON_PRESS) {
826             on_activate();
827             return true;
828         }
829 
830         return Gtk::MenuItem::on_button_press_event(event);
831     }
832 
833 private:
834     int _id;
835 };
836 
837 int ContextMenuItem::choice = 0;
838 
839 static Gtk::Menu *context_menu = NULL, *context_submenu = NULL;
840 
AddContextMenuItem(const char * label,int id)841 void AddContextMenuItem(const char *label, int id) {
842     Gtk::MenuItem *menu_item;
843     if(label)
844         menu_item = new ContextMenuItem(label, id);
845     else
846         menu_item = new Gtk::SeparatorMenuItem();
847 
848     if(id == CONTEXT_SUBMENU) {
849         menu_item->set_submenu(*context_submenu);
850         context_submenu = NULL;
851     }
852 
853     if(context_submenu) {
854         context_submenu->append(*menu_item);
855     } else {
856         if(!context_menu)
857             context_menu = new Gtk::Menu;
858 
859         context_menu->append(*menu_item);
860     }
861 }
862 
CreateContextSubmenu(void)863 void CreateContextSubmenu(void) {
864     if(context_submenu) oops();
865 
866     context_submenu = new Gtk::Menu;
867 }
868 
ShowContextMenu(void)869 int ShowContextMenu(void) {
870     if(!context_menu)
871         return -1;
872 
873     Glib::RefPtr<Glib::MainLoop> loop = Glib::MainLoop::create();
874     context_menu->signal_deactivate().
875         connect(sigc::mem_fun(loop.operator->(), &Glib::MainLoop::quit));
876 
877     ContextMenuItem::choice = -1;
878 
879     context_menu->show_all();
880     context_menu->popup(3, GDK_CURRENT_TIME);
881 
882     loop->run();
883 
884     delete context_menu;
885     context_menu = NULL;
886 
887     return ContextMenuItem::choice;
888 }
889 
890 /* Main menu */
891 
892 template<class MenuItem> class MainMenuItem : public MenuItem {
893 public:
MainMenuItem(const GraphicsWindow::MenuEntry & entry)894     MainMenuItem(const GraphicsWindow::MenuEntry &entry) :
895             MenuItem(), _entry(entry), _synthetic(false) {
896         Glib::ustring label(_entry.label);
897         for(size_t i = 0; i < label.length(); i++) {
898             if(label[i] == '&')
899                 label.replace(i, 1, "_");
900         }
901 
902         guint accel_key = 0;
903         Gdk::ModifierType accel_mods = Gdk::ModifierType();
904         switch(_entry.accel) {
905             case GraphicsWindow::DELETE_KEY:
906             accel_key = GDK_KEY_Delete;
907             break;
908 
909             case GraphicsWindow::ESCAPE_KEY:
910             accel_key = GDK_KEY_Escape;
911             break;
912 
913             case '\t':
914             accel_key = GDK_KEY_Tab;
915             break;
916 
917             default:
918             accel_key = _entry.accel & ~(GraphicsWindow::SHIFT_MASK | GraphicsWindow::CTRL_MASK);
919             if(accel_key > GraphicsWindow::FUNCTION_KEY_BASE &&
920                     accel_key <= GraphicsWindow::FUNCTION_KEY_BASE + 12)
921                 accel_key = GDK_KEY_F1 + (accel_key - GraphicsWindow::FUNCTION_KEY_BASE - 1);
922             else
923                 accel_key = gdk_unicode_to_keyval(accel_key);
924 
925             if(_entry.accel & GraphicsWindow::SHIFT_MASK)
926                 accel_mods |= Gdk::SHIFT_MASK;
927             if(_entry.accel & GraphicsWindow::CTRL_MASK)
928                 accel_mods |= Gdk::CONTROL_MASK;
929         }
930 
931         MenuItem::set_label(label);
932         MenuItem::set_use_underline(true);
933         if(!(accel_key & 0x01000000))
934             MenuItem::set_accel_key(Gtk::AccelKey(accel_key, accel_mods));
935     }
936 
set_active(bool checked)937     void set_active(bool checked) {
938         if(MenuItem::get_active() == checked)
939             return;
940 
941        _synthetic = true;
942         MenuItem::set_active(checked);
943     }
944 
945 protected:
on_activate()946     virtual void on_activate() {
947         MenuItem::on_activate();
948 
949         if(_synthetic)
950             _synthetic = false;
951         else if(!MenuItem::has_submenu() && _entry.fn)
952             _entry.fn(_entry.id);
953     }
954 
955 private:
956     const GraphicsWindow::MenuEntry &_entry;
957     bool _synthetic;
958 };
959 
960 static std::map<int, Gtk::MenuItem *> main_menu_items;
961 
InitMainMenu(Gtk::MenuShell * menu_shell)962 static void InitMainMenu(Gtk::MenuShell *menu_shell) {
963     Gtk::MenuItem *menu_item = NULL;
964     Gtk::MenuShell *levels[5] = {menu_shell, 0};
965 
966     const GraphicsWindow::MenuEntry *entry = &GraphicsWindow::menu[0];
967     int current_level = 0;
968     while(entry->level >= 0) {
969         if(entry->level > current_level) {
970             Gtk::Menu *menu = new Gtk::Menu;
971             menu_item->set_submenu(*menu);
972 
973             if((unsigned)entry->level >= sizeof(levels) / sizeof(levels[0]))
974                 oops();
975 
976             levels[entry->level] = menu;
977         }
978 
979         current_level = entry->level;
980 
981         if(entry->label) {
982             switch(entry->kind) {
983                 case GraphicsWindow::MENU_ITEM_NORMAL:
984                 menu_item = new MainMenuItem<Gtk::MenuItem>(*entry);
985                 break;
986 
987                 case GraphicsWindow::MENU_ITEM_CHECK:
988                 menu_item = new MainMenuItem<Gtk::CheckMenuItem>(*entry);
989                 break;
990 
991                 case GraphicsWindow::MENU_ITEM_RADIO:
992                 MainMenuItem<Gtk::CheckMenuItem> *radio_item =
993                         new MainMenuItem<Gtk::CheckMenuItem>(*entry);
994                 radio_item->set_draw_as_radio(true);
995                 menu_item = radio_item;
996                 break;
997             }
998         } else {
999             menu_item = new Gtk::SeparatorMenuItem();
1000         }
1001 
1002         levels[entry->level]->append(*menu_item);
1003 
1004         main_menu_items[entry->id] = menu_item;
1005 
1006         ++entry;
1007     }
1008 }
1009 
EnableMenuById(int id,bool enabled)1010 void EnableMenuById(int id, bool enabled) {
1011     main_menu_items[id]->set_sensitive(enabled);
1012 }
1013 
CheckMenuById(int id,bool checked)1014 void CheckMenuById(int id, bool checked) {
1015     ((MainMenuItem<Gtk::CheckMenuItem>*)main_menu_items[id])->set_active(checked);
1016 }
1017 
RadioMenuById(int id,bool selected)1018 void RadioMenuById(int id, bool selected) {
1019     SolveSpace::CheckMenuById(id, selected);
1020 }
1021 
1022 class RecentMenuItem : public Gtk::MenuItem {
1023 public:
RecentMenuItem(const Glib::ustring & label,int id)1024     RecentMenuItem(const Glib::ustring& label, int id) :
1025             MenuItem(label), _id(id) {
1026     }
1027 
1028 protected:
on_activate()1029     virtual void on_activate() {
1030         if(_id >= RECENT_OPEN && _id < (RECENT_OPEN + MAX_RECENT))
1031             SolveSpaceUI::MenuFile(_id);
1032         else if(_id >= RECENT_LINK && _id < (RECENT_LINK + MAX_RECENT))
1033             Group::MenuGroup(_id);
1034     }
1035 
1036 private:
1037     int _id;
1038 };
1039 
RefreshRecentMenu(int id,int base)1040 static void RefreshRecentMenu(int id, int base) {
1041     Gtk::MenuItem *recent = static_cast<Gtk::MenuItem*>(main_menu_items[id]);
1042     recent->unset_submenu();
1043 
1044     Gtk::Menu *menu = new Gtk::Menu;
1045     recent->set_submenu(*menu);
1046 
1047     if(std::string(RecentFile[0]).empty()) {
1048         Gtk::MenuItem *placeholder = new Gtk::MenuItem("(no recent files)");
1049         placeholder->set_sensitive(false);
1050         menu->append(*placeholder);
1051     } else {
1052         for(int i = 0; i < MAX_RECENT; i++) {
1053             if(std::string(RecentFile[i]).empty())
1054                 break;
1055 
1056             RecentMenuItem *item = new RecentMenuItem(RecentFile[i], base + i);
1057             menu->append(*item);
1058         }
1059     }
1060 
1061     menu->show_all();
1062 }
1063 
RefreshRecentMenus(void)1064 void RefreshRecentMenus(void) {
1065     RefreshRecentMenu(GraphicsWindow::MNU_OPEN_RECENT, RECENT_OPEN);
1066     RefreshRecentMenu(GraphicsWindow::MNU_GROUP_RECENT, RECENT_LINK);
1067 }
1068 
1069 /* Save/load */
1070 
ConvertFilters(std::string active,const FileFilter ssFilters[],Gtk::FileChooser * chooser)1071 static std::string ConvertFilters(std::string active, const FileFilter ssFilters[],
1072                                   Gtk::FileChooser *chooser) {
1073     for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) {
1074 #ifdef HAVE_GTK3
1075         Glib::RefPtr<Gtk::FileFilter> filter = Gtk::FileFilter::create();
1076 #else
1077         Gtk::FileFilter *filter = new Gtk::FileFilter;
1078 #endif
1079         filter->set_name(ssFilter->name);
1080 
1081         bool is_active = false;
1082         std::string desc = "";
1083         for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) {
1084             std::string pattern = "*." + std::string(*ssPattern);
1085             filter->add_pattern(pattern);
1086             filter->add_pattern(Glib::ustring(pattern).uppercase());
1087             if(active == "")
1088                 active = pattern.substr(2);
1089             if("*." + active == pattern)
1090                 is_active = true;
1091             if(desc == "")
1092                 desc = pattern;
1093             else
1094                 desc += ", " + pattern;
1095         }
1096         filter->set_name(filter->get_name() + " (" + desc + ")");
1097 
1098 #ifdef HAVE_GTK3
1099         chooser->add_filter(filter);
1100         if(is_active)
1101             chooser->set_filter(filter);
1102 #else
1103         chooser->add_filter(*filter);
1104         if(is_active)
1105             chooser->set_filter(*filter);
1106 #endif
1107     }
1108 
1109     return active;
1110 }
1111 
GetOpenFile(std::string * filename,const std::string & activeOrEmpty,const FileFilter filters[])1112 bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty,
1113                  const FileFilter filters[]) {
1114     Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Open File");
1115     chooser.set_filename(*filename);
1116     chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
1117     chooser.add_button("_Open", Gtk::RESPONSE_OK);
1118     chooser.set_current_folder(CnfThawString("", "FileChooserPath"));
1119 
1120     ConvertFilters(activeOrEmpty, filters, &chooser);
1121 
1122     if(chooser.run() == Gtk::RESPONSE_OK) {
1123         CnfFreezeString(chooser.get_current_folder(), "FileChooserPath");
1124         *filename = chooser.get_filename();
1125         return true;
1126     } else {
1127         return false;
1128     }
1129 }
1130 
1131 /* Glib::path_get_basename got /removed/ in 3.0?! Come on */
Basename(std::string filename)1132 static std::string Basename(std::string filename) {
1133     int slash = filename.rfind('/');
1134     if(slash >= 0)
1135         return filename.substr(slash + 1, filename.length());
1136     return "";
1137 }
1138 
ChooserFilterChanged(Gtk::FileChooserDialog * chooser)1139 static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser)
1140 {
1141     /* Extract the pattern from the filter. GtkFileFilter doesn't provide
1142        any way to list the patterns, so we extract it from the filter name.
1143        Gross. */
1144     std::string filter_name = chooser->get_filter()->get_name();
1145     int lparen = filter_name.rfind('(') + 1;
1146     int rdelim = filter_name.find(',', lparen);
1147     if(rdelim < 0)
1148         rdelim = filter_name.find(')', lparen);
1149     if(lparen < 0 || rdelim < 0)
1150         oops();
1151 
1152     std::string extension = filter_name.substr(lparen, rdelim - lparen);
1153     if(extension == "*")
1154         return;
1155 
1156     if(extension.length() > 2 && extension.substr(0, 2) == "*.")
1157         extension = extension.substr(2, extension.length() - 2);
1158 
1159     std::string basename = Basename(chooser->get_filename());
1160     int dot = basename.rfind('.');
1161     if(dot >= 0) {
1162         basename.replace(dot + 1, basename.length() - dot - 1, extension);
1163         chooser->set_current_name(basename);
1164     } else {
1165         chooser->set_current_name(basename + "." + extension);
1166     }
1167 }
1168 
GetSaveFile(std::string * filename,const std::string & activeOrEmpty,const FileFilter filters[])1169 bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty,
1170                  const FileFilter filters[]) {
1171     Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Save File",
1172                                    Gtk::FILE_CHOOSER_ACTION_SAVE);
1173     chooser.set_do_overwrite_confirmation(true);
1174     chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
1175     chooser.add_button("_Save", Gtk::RESPONSE_OK);
1176 
1177     std::string active = ConvertFilters(activeOrEmpty, filters, &chooser);
1178 
1179     chooser.set_current_folder(CnfThawString("", "FileChooserPath"));
1180     chooser.set_current_name(std::string("untitled.") + active);
1181 
1182     /* Gtk's dialog doesn't change the extension when you change the filter,
1183        and makes it extremely hard to do so. Gtk is garbage. */
1184     chooser.property_filter().signal_changed().
1185        connect(sigc::bind(sigc::ptr_fun(&ChooserFilterChanged), &chooser));
1186 
1187     if(chooser.run() == Gtk::RESPONSE_OK) {
1188         CnfFreezeString(chooser.get_current_folder(), "FileChooserPath");
1189         *filename = chooser.get_filename();
1190         return true;
1191     } else {
1192         return false;
1193     }
1194 }
1195 
SaveFileYesNoCancel(void)1196 DialogChoice SaveFileYesNoCancel(void) {
1197     Glib::ustring message =
1198         "The file has changed since it was last saved.\n"
1199         "Do you want to save the changes?";
1200     Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION,
1201                               Gtk::BUTTONS_NONE, /*is_modal*/ true);
1202     dialog.set_title("SolveSpace - Modified File");
1203     dialog.add_button("_Save", Gtk::RESPONSE_YES);
1204     dialog.add_button("Do_n't Save", Gtk::RESPONSE_NO);
1205     dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
1206 
1207     switch(dialog.run()) {
1208         case Gtk::RESPONSE_YES:
1209         return DIALOG_YES;
1210 
1211         case Gtk::RESPONSE_NO:
1212         return DIALOG_NO;
1213 
1214         case Gtk::RESPONSE_CANCEL:
1215         default:
1216         return DIALOG_CANCEL;
1217     }
1218 }
1219 
LoadAutosaveYesNo(void)1220 DialogChoice LoadAutosaveYesNo(void) {
1221     Glib::ustring message =
1222         "An autosave file is availible for this project.\n"
1223         "Do you want to load the autosave file instead?";
1224     Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION,
1225                               Gtk::BUTTONS_NONE, /*is_modal*/ true);
1226     dialog.set_title("SolveSpace - Autosave Available");
1227     dialog.add_button("_Load autosave", Gtk::RESPONSE_YES);
1228     dialog.add_button("Do_n't Load", Gtk::RESPONSE_NO);
1229 
1230     switch(dialog.run()) {
1231         case Gtk::RESPONSE_YES:
1232         return DIALOG_YES;
1233 
1234         case Gtk::RESPONSE_NO:
1235         default:
1236         return DIALOG_NO;
1237     }
1238 }
1239 
LocateImportedFileYesNoCancel(const std::string & filename,bool canCancel)1240 DialogChoice LocateImportedFileYesNoCancel(const std::string &filename,
1241                                            bool canCancel) {
1242     Glib::ustring message =
1243         "The linked file " + filename + " is not present.\n"
1244         "Do you want to locate it manually?\n"
1245         "If you select \"No\", any geometry that depends on "
1246         "the missing file will be removed.";
1247     Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION,
1248                               Gtk::BUTTONS_NONE, /*is_modal*/ true);
1249     dialog.set_title("SolveSpace - Missing File");
1250     dialog.add_button("_Yes", Gtk::RESPONSE_YES);
1251     dialog.add_button("_No", Gtk::RESPONSE_NO);
1252     if(canCancel)
1253         dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
1254 
1255     switch(dialog.run()) {
1256         case Gtk::RESPONSE_YES:
1257         return DIALOG_YES;
1258 
1259         case Gtk::RESPONSE_NO:
1260         return DIALOG_NO;
1261 
1262         case Gtk::RESPONSE_CANCEL:
1263         default:
1264         return DIALOG_CANCEL;
1265     }
1266 }
1267 
1268 /* Text window */
1269 
1270 class TextWidget : public GlWidget {
1271 public:
1272 #ifdef HAVE_GTK3
TextWidget(Glib::RefPtr<Gtk::Adjustment> adjustment)1273     TextWidget(Glib::RefPtr<Gtk::Adjustment> adjustment) : _adjustment(adjustment) {
1274 #else
1275     TextWidget(Gtk::Adjustment* adjustment) : _adjustment(adjustment) {
1276 #endif
1277         set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK |
1278                    Gdk::LEAVE_NOTIFY_MASK);
1279     }
1280 
1281     void set_cursor_hand(bool is_hand) {
1282         Glib::RefPtr<Gdk::Window> gdkwin = get_window();
1283         if(gdkwin) { // returns NULL if not realized
1284             Gdk::CursorType type = is_hand ? Gdk::HAND1 : Gdk::ARROW;
1285 #ifdef HAVE_GTK3
1286             gdkwin->set_cursor(Gdk::Cursor::create(type));
1287 #else
1288             gdkwin->set_cursor(Gdk::Cursor(type));
1289 #endif
1290         }
1291     }
1292 
1293 protected:
1294     virtual void on_gl_draw() {
1295         SS.TW.Paint();
1296     }
1297 
1298     virtual bool on_motion_notify_event(GdkEventMotion *event) {
1299         SS.TW.MouseEvent(/*leftClick*/ false,
1300                          /*leftDown*/ event->state & GDK_BUTTON1_MASK,
1301                          event->x, event->y);
1302 
1303         return true;
1304     }
1305 
1306     virtual bool on_button_press_event(GdkEventButton *event) {
1307         SS.TW.MouseEvent(/*leftClick*/ event->type == GDK_BUTTON_PRESS,
1308                          /*leftDown*/ event->state & GDK_BUTTON1_MASK,
1309                          event->x, event->y);
1310 
1311         return true;
1312     }
1313 
1314     virtual bool on_scroll_event(GdkEventScroll *event) {
1315         _adjustment->set_value(_adjustment->get_value() +
1316                 DeltaYOfScrollEvent(event) * _adjustment->get_page_increment());
1317 
1318         return true;
1319     }
1320 
1321     virtual bool on_leave_notify_event (GdkEventCrossing *) {
1322         SS.TW.MouseLeave();
1323 
1324         return true;
1325     }
1326 
1327 private:
1328 #ifdef HAVE_GTK3
1329     Glib::RefPtr<Gtk::Adjustment> _adjustment;
1330 #else
1331     Gtk::Adjustment *_adjustment;
1332 #endif
1333 };
1334 
1335 class TextWindowGtk : public Gtk::Window {
1336 public:
TextWindowGtk()1337     TextWindowGtk() : _scrollbar(), _widget(_scrollbar.get_adjustment()),
1338                       _overlay(_widget), _box() {
1339         set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY);
1340         set_skip_taskbar_hint(true);
1341         set_skip_pager_hint(true);
1342         set_title("SolveSpace - Property Browser");
1343         set_default_size(420, 300);
1344 
1345         _box.pack_start(_overlay, true, true);
1346         _box.pack_start(_scrollbar, false, true);
1347         add(_box);
1348 
1349         _scrollbar.get_adjustment()->signal_value_changed().
1350             connect(sigc::mem_fun(this, &TextWindowGtk::on_scrollbar_value_changed));
1351 
1352         _overlay.signal_editing_done().
1353             connect(sigc::mem_fun(this, &TextWindowGtk::on_editing_done));
1354 
1355         _overlay.get_entry().signal_motion_notify_event().
1356             connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_motion_notify_event));
1357         _overlay.get_entry().signal_button_press_event().
1358             connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_button_press_event));
1359     }
1360 
get_scrollbar()1361     Gtk::VScrollbar &get_scrollbar() {
1362         return _scrollbar;
1363     }
1364 
get_widget()1365     TextWidget &get_widget() {
1366         return _widget;
1367     }
1368 
get_overlay()1369     EditorOverlay &get_overlay() {
1370         return _overlay;
1371     }
1372 
1373 protected:
on_show()1374     virtual void on_show() {
1375         Gtk::Window::on_show();
1376 
1377         CnfThawWindowPos(this, "TextWindow");
1378     }
1379 
on_hide()1380     virtual void on_hide() {
1381         CnfFreezeWindowPos(this, "TextWindow");
1382 
1383         Gtk::Window::on_hide();
1384     }
1385 
on_delete_event(GdkEventAny *)1386     virtual bool on_delete_event(GdkEventAny *) {
1387         /* trigger the action and ignore the request */
1388         GraphicsWindow::MenuView(GraphicsWindow::MNU_SHOW_TEXT_WND);
1389 
1390         return false;
1391     }
1392 
on_scrollbar_value_changed()1393     virtual void on_scrollbar_value_changed() {
1394         SS.TW.ScrollbarEvent(_scrollbar.get_adjustment()->get_value());
1395     }
1396 
on_editing_done(Glib::ustring value)1397     virtual void on_editing_done(Glib::ustring value) {
1398         SS.TW.EditControlDone(value.c_str());
1399     }
1400 
on_editor_motion_notify_event(GdkEventMotion * event)1401     virtual bool on_editor_motion_notify_event(GdkEventMotion *event) {
1402         return _widget.event((GdkEvent*) event);
1403     }
1404 
on_editor_button_press_event(GdkEventButton * event)1405     virtual bool on_editor_button_press_event(GdkEventButton *event) {
1406         return _widget.event((GdkEvent*) event);
1407     }
1408 
on_key_press_event(GdkEventKey * event)1409     virtual bool on_key_press_event(GdkEventKey *event) {
1410         if(GW->emulate_key_press(event)) {
1411             return true;
1412         }
1413 
1414         return Gtk::Window::on_key_press_event(event);
1415     }
1416 
1417 private:
1418     Gtk::VScrollbar _scrollbar;
1419     TextWidget _widget;
1420     EditorOverlay _overlay;
1421     Gtk::HBox _box;
1422 };
1423 
1424 std::unique_ptr<TextWindowGtk> TW;
1425 
ShowTextWindow(bool visible)1426 void ShowTextWindow(bool visible) {
1427     if(visible)
1428         TW->show();
1429     else
1430         TW->hide();
1431 }
1432 
GetTextWindowSize(int * w,int * h)1433 void GetTextWindowSize(int *w, int *h) {
1434     Gdk::Rectangle allocation = TW->get_widget().get_allocation();
1435     *w = allocation.get_width();
1436     *h = allocation.get_height();
1437 }
1438 
InvalidateText(void)1439 void InvalidateText(void) {
1440     TW->get_widget().queue_draw();
1441 }
1442 
MoveTextScrollbarTo(int pos,int maxPos,int page)1443 void MoveTextScrollbarTo(int pos, int maxPos, int page) {
1444     TW->get_scrollbar().get_adjustment()->configure(pos, 0, maxPos, 1, 10, page);
1445 }
1446 
SetMousePointerToHand(bool is_hand)1447 void SetMousePointerToHand(bool is_hand) {
1448     TW->get_widget().set_cursor_hand(is_hand);
1449 }
1450 
ShowTextEditControl(int x,int y,const std::string & val)1451 void ShowTextEditControl(int x, int y, const std::string &val) {
1452     TW->get_overlay().start_editing(x, y, TextWindow::CHAR_HEIGHT, /*is_monospace=*/true, 30, val);
1453 }
1454 
HideTextEditControl(void)1455 void HideTextEditControl(void) {
1456     TW->get_overlay().stop_editing();
1457 }
1458 
TextEditControlIsVisible(void)1459 bool TextEditControlIsVisible(void) {
1460     return TW->get_overlay().is_editing();
1461 }
1462 
1463 /* Miscellanea */
1464 
1465 
DoMessageBox(const char * message,int rows,int cols,bool error)1466 void DoMessageBox(const char *message, int rows, int cols, bool error) {
1467     Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true,
1468                               error ? Gtk::MESSAGE_ERROR : Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK,
1469                               /*is_modal*/ true);
1470     dialog.set_title(error ? "SolveSpace - Error" : "SolveSpace - Message");
1471     dialog.run();
1472 }
1473 
OpenWebsite(const char * url)1474 void OpenWebsite(const char *url) {
1475     gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL);
1476 }
1477 
1478 /* fontconfig is already initialized by GTK */
GetFontFiles()1479 std::vector<std::string> GetFontFiles() {
1480     std::vector<std::string> fonts;
1481 
1482     FcPattern   *pat = FcPatternCreate();
1483     FcObjectSet *os  = FcObjectSetBuild(FC_FILE, (char *)0);
1484     FcFontSet   *fs  = FcFontList(0, pat, os);
1485 
1486     for(int i = 0; i < fs->nfont; i++) {
1487         FcChar8 *filenameFC = FcPatternFormat(fs->fonts[i], (const FcChar8*) "%{file}");
1488         std::string filename = (char*) filenameFC;
1489         fonts.push_back(filename);
1490         FcStrFree(filenameFC);
1491     }
1492 
1493     FcFontSetDestroy(fs);
1494     FcObjectSetDestroy(os);
1495     FcPatternDestroy(pat);
1496 
1497     return fonts;
1498 }
1499 
1500 /* Space Navigator support */
1501 
1502 #ifdef HAVE_SPACEWARE
GdkSpnavFilter(GdkXEvent * gxevent,GdkEvent *,gpointer)1503 static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *, gpointer) {
1504     XEvent *xevent = (XEvent*) gxevent;
1505 
1506     spnav_event sev;
1507     if(!spnav_x11_event(xevent, &sev))
1508         return GDK_FILTER_CONTINUE;
1509 
1510     switch(sev.type) {
1511         case SPNAV_EVENT_MOTION:
1512             SS.GW.SpaceNavigatorMoved(
1513                 (double)sev.motion.x,
1514                 (double)sev.motion.y,
1515                 (double)sev.motion.z  * -1.0,
1516                 (double)sev.motion.rx *  0.001,
1517                 (double)sev.motion.ry *  0.001,
1518                 (double)sev.motion.rz * -0.001,
1519                 xevent->xmotion.state & ShiftMask);
1520             break;
1521 
1522         case SPNAV_EVENT_BUTTON:
1523             if(!sev.button.press && sev.button.bnum == 0) {
1524                 SS.GW.SpaceNavigatorButtonUp();
1525             }
1526             break;
1527     }
1528 
1529     return GDK_FILTER_REMOVE;
1530 }
1531 #endif
1532 
1533 /* Application lifecycle */
1534 
ExitNow(void)1535 void ExitNow(void) {
1536     GW->hide();
1537     TW->hide();
1538 }
1539 };
1540 
main(int argc,char ** argv)1541 int main(int argc, char** argv) {
1542     /* It would in principle be possible to judiciously use
1543        Glib::filename_{from,to}_utf8, but it's not really worth
1544        the effort.
1545        The setlocale() call is necessary for Glib::get_charset()
1546        to detect the system character set; otherwise it thinks
1547        it is always ANSI_X3.4-1968.
1548        We set it back to C after all.  */
1549     setlocale(LC_ALL, "");
1550     if(!Glib::get_charset()) {
1551         std::cerr << "Sorry, only UTF-8 locales are supported." << std::endl;
1552         return 1;
1553     }
1554     setlocale(LC_ALL, "C");
1555 
1556     /* If we don't do this, gtk_init will set the C standard library
1557        locale, and printf will format floats using ",". We will then
1558        fail to parse these. Also, many text window lines will become
1559        ambiguous. */
1560     gtk_disable_setlocale();
1561 
1562     Gtk::Main main(argc, argv);
1563 
1564 #ifdef HAVE_SPACEWARE
1565     gdk_window_add_filter(NULL, GdkSpnavFilter, NULL);
1566 #endif
1567 
1568     CnfLoad();
1569 
1570     TW.reset(new TextWindowGtk);
1571     GW.reset(new GraphicsWindowGtk);
1572     InitMainMenu(&GW->get_menubar());
1573     GW->get_menubar().accelerate(*TW);
1574     TW->set_transient_for(*GW);
1575 
1576     TW->show_all();
1577     GW->show_all();
1578 
1579 #ifdef HAVE_SPACEWARE
1580 #ifdef HAVE_GTK3
1581     // We don't care if it can't be opened; just continue without.
1582     spnav_x11_open(gdk_x11_get_default_xdisplay(),
1583                    gdk_x11_window_get_xid(GW->get_window()->gobj()));
1584 #else
1585     spnav_x11_open(gdk_x11_get_default_xdisplay(),
1586                    GDK_WINDOW_XWINDOW(GW->get_window()->gobj()));
1587 #endif
1588 #endif
1589 
1590     SS.Init();
1591 
1592     if(argc >= 2) {
1593         if(argc > 2) {
1594             std::cerr << "Only the first file passed on command line will be opened."
1595                       << std::endl;
1596         }
1597 
1598         /* Make sure the argument is valid UTF-8. */
1599         SS.OpenFile(Glib::ustring(argv[1]));
1600     }
1601 
1602     main.run(*GW);
1603 
1604     TW.reset();
1605     GW.reset();
1606 
1607     SK.Clear();
1608     SS.Clear();
1609 
1610     return 0;
1611 }
1612