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