//----------------------------------------------------------------------------- // Our main() function, and GTK2/3-specific stuff to set up our windows and // otherwise handle our interface to the operating system. Everything // outside platform/... should be standard C++ and OpenGL. // // Copyright 2015 //----------------------------------------------------------------------------- #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_GTK3 #include #else #include #endif #include #include #include #include #include #include "solvespace.h" #include "config.h" #include "gloffscreen.h" #ifdef HAVE_SPACEWARE # include # ifndef SI_APP_FIT_BUTTON # define SI_APP_FIT_BUTTON 31 # endif #endif namespace SolveSpace { /* Settings */ /* Why not just use GSettings? Two reasons. It doesn't allow to easily see whether the setting had the default value, and it requires to install a schema globally. */ static json_object *settings = NULL; static std::string CnfPrepare() { // Refer to http://standards.freedesktop.org/basedir-spec/latest/ std::string dir; if(getenv("XDG_CONFIG_HOME")) { dir = std::string(getenv("XDG_CONFIG_HOME")) + "/solvespace"; } else if(getenv("HOME")) { dir = std::string(getenv("HOME")) + "/.config/solvespace"; } else { dbp("neither XDG_CONFIG_HOME nor HOME are set"); return ""; } struct stat st; if(stat(dir.c_str(), &st)) { if(errno == ENOENT) { if(mkdir(dir.c_str(), 0777)) { dbp("cannot mkdir %s: %s", dir.c_str(), strerror(errno)); return ""; } } else { dbp("cannot stat %s: %s", dir.c_str(), strerror(errno)); return ""; } } else if(!S_ISDIR(st.st_mode)) { dbp("%s is not a directory", dir.c_str()); return ""; } return dir + "/settings.json"; } static void CnfLoad() { std::string path = CnfPrepare(); if(path.empty()) return; if(settings) json_object_put(settings); // deallocate settings = json_object_from_file(path.c_str()); if(!settings) { if(errno != ENOENT) dbp("cannot load settings: %s", strerror(errno)); settings = json_object_new_object(); } } static void CnfSave() { std::string path = CnfPrepare(); if(path.empty()) return; /* json-c <0.12 has the first argument non-const here */ if(json_object_to_file_ext((char*) path.c_str(), settings, JSON_C_TO_STRING_PRETTY)) dbp("cannot save settings: %s", strerror(errno)); } void CnfFreezeInt(uint32_t val, const std::string &key) { struct json_object *jval = json_object_new_int(val); json_object_object_add(settings, key.c_str(), jval); CnfSave(); } uint32_t CnfThawInt(uint32_t val, const std::string &key) { struct json_object *jval; if(json_object_object_get_ex(settings, key.c_str(), &jval)) return json_object_get_int(jval); else return val; } void CnfFreezeFloat(float val, const std::string &key) { struct json_object *jval = json_object_new_double(val); json_object_object_add(settings, key.c_str(), jval); CnfSave(); } float CnfThawFloat(float val, const std::string &key) { struct json_object *jval; if(json_object_object_get_ex(settings, key.c_str(), &jval)) return json_object_get_double(jval); else return val; } void CnfFreezeString(const std::string &val, const std::string &key) { struct json_object *jval = json_object_new_string(val.c_str()); json_object_object_add(settings, key.c_str(), jval); CnfSave(); } std::string CnfThawString(const std::string &val, const std::string &key) { struct json_object *jval; if(json_object_object_get_ex(settings, key.c_str(), &jval)) return json_object_get_string(jval); return val; } static void CnfFreezeWindowPos(Gtk::Window *win, const std::string &key) { int x, y, w, h; win->get_position(x, y); win->get_size(w, h); CnfFreezeInt(x, key + "_left"); CnfFreezeInt(y, key + "_top"); CnfFreezeInt(w, key + "_width"); CnfFreezeInt(h, key + "_height"); } static void CnfThawWindowPos(Gtk::Window *win, const std::string &key) { int x, y, w, h; win->get_position(x, y); win->get_size(w, h); x = CnfThawInt(x, key + "_left"); y = CnfThawInt(y, key + "_top"); w = CnfThawInt(w, key + "_width"); h = CnfThawInt(h, key + "_height"); win->move(x, y); win->resize(w, h); } /* Timers */ int64_t GetMilliseconds(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return 1000 * (uint64_t) ts.tv_sec + ts.tv_nsec / 1000000; } static bool TimerCallback() { SS.GW.TimerCallback(); SS.TW.TimerCallback(); return false; } void SetTimerFor(int milliseconds) { Glib::signal_timeout().connect(&TimerCallback, milliseconds); } static bool AutosaveTimerCallback() { SS.Autosave(); return false; } void SetAutosaveTimerFor(int minutes) { Glib::signal_timeout().connect(&AutosaveTimerCallback, minutes * 60 * 1000); } static bool LaterCallback() { SS.DoLater(); return false; } void ScheduleLater() { Glib::signal_idle().connect(&LaterCallback); } /* GL wrapper */ #define GL_CHECK() \ do { \ int err = (int)glGetError(); \ if(err) dbp("%s:%d: glGetError() == 0x%X %s", \ __FILE__, __LINE__, err, gluErrorString(err)); \ } while (0) class GlWidget : public Gtk::DrawingArea { public: GlWidget() : _offscreen(NULL) { _xdisplay = gdk_x11_get_default_xdisplay(); int glxmajor, glxminor; ssassert(glXQueryVersion(_xdisplay, &glxmajor, &glxminor), "Expected OpenGL to be available"); ssassert(glxmajor > 1 || (glxmajor == 1 && glxminor >= 3), "Expected GLX >= 1.3"); static int fbconfig_attrs[] = { GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_DEPTH_SIZE, 24, None }; int fbconfig_num = 0; GLXFBConfig *fbconfigs = glXChooseFBConfig(_xdisplay, DefaultScreen(_xdisplay), fbconfig_attrs, &fbconfig_num); ssassert(fbconfigs && fbconfig_num > 0, "Expected an available framebuffer configuration"); /* prefer FBConfigs with depth of 32; * Mesa software rasterizer explodes with a BadMatch without this; * without this, Intel on Mesa flickers horribly for some reason. this does not seem to affect other rasterizers (ie NVidia). see this Mesa bug: http://lists.freedesktop.org/archives/mesa-dev/2015-January/074693.html */ GLXFBConfig fbconfig = fbconfigs[0]; for(int i = 0; i < fbconfig_num; i++) { XVisualInfo *visual_info = glXGetVisualFromFBConfig(_xdisplay, fbconfigs[i]); /* some GL visuals, notably on Chromium GL, do not have an associated X visual; this is not an obstacle as we always render offscreen. */ if(!visual_info) continue; int depth = visual_info->depth; XFree(visual_info); if(depth == 32) { fbconfig = fbconfigs[i]; break; } } _glcontext = glXCreateNewContext(_xdisplay, fbconfig, GLX_RGBA_TYPE, 0, True); ssassert(_glcontext != NULL, "Cannot create an OpenGL context"); XFree(fbconfigs); /* create a dummy X window to create a rendering context against. we could use a Pbuffer, but some implementations (Chromium GL) don't support these. we could use an existing window, but some implementations (Chromium GL... do you see a pattern?) do really strange things, i.e. draw a black rectangle on the very front of the desktop if you do this. */ _xwindow = XCreateSimpleWindow(_xdisplay, XRootWindow(_xdisplay, gdk_x11_get_default_screen()), /*x*/ 0, /*y*/ 0, /*width*/ 1, /*height*/ 1, /*border_width*/ 0, /*border*/ 0, /*background*/ 0); } ~GlWidget() { glXMakeCurrent(_xdisplay, None, NULL); XDestroyWindow(_xdisplay, _xwindow); delete _offscreen; glXDestroyContext(_xdisplay, _glcontext); } protected: /* Draw on a GLX framebuffer object, then read pixels out and draw them on the Cairo context. Slower, but you get to overlay nice widgets. */ #ifdef HAVE_GTK3 bool on_draw(const Cairo::RefPtr &cr) override { #else bool on_expose_event(GdkEventExpose *) override { const Cairo::RefPtr &cr = get_window()->create_cairo_context(); #endif ssassert(glXMakeCurrent(_xdisplay, _xwindow, _glcontext), "Cannot make OpenGL context current"); if(!_offscreen) _offscreen = new GLOffscreen; Gdk::Rectangle allocation = get_allocation(); ssassert(_offscreen->begin(allocation.get_width(), allocation.get_height()), "Cannot allocate offscreen rendering buffer"); on_gl_draw(); glFlush(); GL_CHECK(); Cairo::RefPtr surface = Cairo::ImageSurface::create( _offscreen->end(), Cairo::FORMAT_RGB24, allocation.get_width(), allocation.get_height(), allocation.get_width() * 4); cr->set_source(surface, 0, 0); cr->paint(); surface->finish(); return true; } virtual void on_gl_draw() = 0; private: Display *_xdisplay; GLXContext _glcontext; GLOffscreen *_offscreen; ::Window _xwindow; }; /* Editor overlay */ class EditorOverlay : public Gtk::Fixed { public: EditorOverlay(Gtk::Widget &underlay) : _underlay(underlay) { set_size_request(0, 0); add(_underlay); _entry.set_no_show_all(true); _entry.set_has_frame(false); add(_entry); _entry.signal_activate(). connect(sigc::mem_fun(this, &EditorOverlay::on_activate)); } void start_editing(int x, int y, int font_height, bool is_monospace, int minWidthChars, const std::string &val) { Pango::FontDescription font_desc; font_desc.set_family(is_monospace ? "monospace" : "normal"); font_desc.set_absolute_size(font_height * Pango::SCALE); #ifdef HAVE_GTK3 /* For some reason override_font doesn't take screen DPI into account on GTK3 when working with font descriptors specified in absolute sizes; modify_font does on GTK2. */ Pango::FontDescription override_font_desc(font_desc); double dpi = get_screen()->get_resolution(); override_font_desc.set_size(font_height * (int)(72.0 / dpi) * Pango::SCALE); _entry.override_font(override_font_desc); #else _entry.modify_font(font_desc); #endif /* y coordinate denotes baseline */ Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc); y -= font_metrics.get_ascent() / Pango::SCALE; Glib::RefPtr layout = Pango::Layout::create(get_pango_context()); layout->set_font_description(font_desc); layout->set_text(val + " "); /* avoid scrolling */ int width = layout->get_logical_extents().get_width(); #ifdef HAVE_GTK3 Gtk::Border border = _entry.get_style_context()->get_padding(); move(_entry, x - border.get_left(), y - border.get_top()); _entry.set_width_chars(minWidthChars); _entry.set_size_request(width / Pango::SCALE, -1); #else /* We need _gtk_entry_effective_inner_border, but it's not in the public API, so emulate its logic. */ Gtk::Border border = { 2, 2, 2, 2 }, *style_border; gtk_widget_style_get(GTK_WIDGET(_entry.gobj()), "inner-border", &style_border, NULL); if(style_border) border = *style_border; move(_entry, x - border.left, y - border.top); /* This is what set_width_chars does. */ int minWidth = minWidthChars * std::max(font_metrics.get_approximate_digit_width(), font_metrics.get_approximate_char_width()); _entry.set_size_request(std::max(width, minWidth) / Pango::SCALE, -1); #endif _entry.set_text(val); if(!_entry.is_visible()) { _entry.show(); _entry.grab_focus(); _entry.add_modal_grab(); } } void stop_editing() { if(_entry.is_visible()) _entry.remove_modal_grab(); _entry.hide(); } bool is_editing() const { return _entry.is_visible(); } sigc::signal signal_editing_done() { return _signal_editing_done; } Gtk::Entry &get_entry() { return _entry; } protected: bool on_key_press_event(GdkEventKey *event) override { if(event->keyval == GDK_KEY_Escape) { stop_editing(); return true; } return false; } void on_size_allocate(Gtk::Allocation& allocation) override { Gtk::Fixed::on_size_allocate(allocation); _underlay.size_allocate(allocation); } void on_activate() { _signal_editing_done(_entry.get_text()); } private: Gtk::Widget &_underlay; Gtk::Entry _entry; sigc::signal _signal_editing_done; }; /* Graphics window */ double DeltaYOfScrollEvent(GdkEventScroll *event) { #ifdef HAVE_GTK3 double delta_y = event->delta_y; #else double delta_y = 0; #endif if(delta_y == 0) { switch(event->direction) { case GDK_SCROLL_UP: delta_y = -1; break; case GDK_SCROLL_DOWN: delta_y = 1; break; default: /* do nothing */ return false; } } return delta_y; } class GraphicsWidget : public GlWidget { public: GraphicsWidget() { set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | Gdk::SCROLL_MASK | Gdk::LEAVE_NOTIFY_MASK); set_double_buffered(true); } protected: bool on_configure_event(GdkEventConfigure *event) override { _w = event->width; _h = event->height; return GlWidget::on_configure_event(event);; } void on_gl_draw() override { SS.GW.Paint(); } bool on_motion_notify_event(GdkEventMotion *event) override { int x, y; ij_to_xy(event->x, event->y, x, y); SS.GW.MouseMoved(x, y, event->state & GDK_BUTTON1_MASK, event->state & GDK_BUTTON2_MASK, event->state & GDK_BUTTON3_MASK, event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK); return true; } bool on_button_press_event(GdkEventButton *event) override { int x, y; ij_to_xy(event->x, event->y, x, y); switch(event->button) { case 1: if(event->type == GDK_BUTTON_PRESS) SS.GW.MouseLeftDown(x, y); else if(event->type == GDK_2BUTTON_PRESS) SS.GW.MouseLeftDoubleClick(x, y); break; case 2: case 3: SS.GW.MouseMiddleOrRightDown(x, y); break; } return true; } bool on_button_release_event(GdkEventButton *event) override { int x, y; ij_to_xy(event->x, event->y, x, y); switch(event->button) { case 1: SS.GW.MouseLeftUp(x, y); break; case 3: SS.GW.MouseRightUp(x, y); break; } return true; } bool on_scroll_event(GdkEventScroll *event) override { int x, y; ij_to_xy(event->x, event->y, x, y); SS.GW.MouseScroll(x, y, (int)-DeltaYOfScrollEvent(event)); return true; } bool on_leave_notify_event (GdkEventCrossing *) override { SS.GW.MouseLeave(); return true; } private: int _w, _h; void ij_to_xy(double i, double j, int &x, int &y) { // Convert to xy (vs. ij) style coordinates, // with (0, 0) at center x = (int)i - _w / 2; y = _h / 2 - (int)j; } }; class GraphicsWindowGtk : public Gtk::Window { public: GraphicsWindowGtk() : _overlay(_widget), _is_fullscreen(false) { set_default_size(900, 600); _box.pack_start(_menubar, false, true); _box.pack_start(_overlay, true, true); add(_box); _overlay.signal_editing_done(). connect(sigc::mem_fun(this, &GraphicsWindowGtk::on_editing_done)); } GraphicsWidget &get_widget() { return _widget; } EditorOverlay &get_overlay() { return _overlay; } Gtk::MenuBar &get_menubar() { return _menubar; } bool is_fullscreen() const { return _is_fullscreen; } bool emulate_key_press(GdkEventKey *event) { return on_key_press_event(event); } protected: void on_show() override { Gtk::Window::on_show(); CnfThawWindowPos(this, "GraphicsWindow"); } void on_hide() override { CnfFreezeWindowPos(this, "GraphicsWindow"); Gtk::Window::on_hide(); } bool on_delete_event(GdkEventAny *) override { if(!SS.OkayToStartNewFile()) return true; SS.Exit(); return true; } bool on_window_state_event(GdkEventWindowState *event) override { _is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; /* The event arrives too late for the caller of ToggleFullScreen to notice state change; and it's possible that the WM will refuse our request, so we can't just toggle the saved state */ SS.GW.EnsureValidActives(); return Gtk::Window::on_window_state_event(event); } bool on_key_press_event(GdkEventKey *event) override { int chr; switch(event->keyval) { case GDK_KEY_Escape: chr = GraphicsWindow::ESCAPE_KEY; break; case GDK_KEY_Delete: chr = GraphicsWindow::DELETE_KEY; break; case GDK_KEY_Tab: chr = '\t'; break; case GDK_KEY_BackSpace: case GDK_KEY_Back: chr = '\b'; break; default: if(event->keyval >= GDK_KEY_F1 && event->keyval <= GDK_KEY_F12) { chr = GraphicsWindow::FUNCTION_KEY_BASE + (event->keyval - GDK_KEY_F1); } else { chr = gdk_keyval_to_unicode(event->keyval); } } if(event->state & GDK_SHIFT_MASK){ chr |= GraphicsWindow::SHIFT_MASK; } if(event->state & GDK_CONTROL_MASK) { chr |= GraphicsWindow::CTRL_MASK; } if(chr && SS.GW.KeyDown(chr)) { return true; } if(chr == '\t') { // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=123994. GraphicsWindow::MenuView(Command::SHOW_TEXT_WND); return true; } return Gtk::Window::on_key_press_event(event); } void on_editing_done(Glib::ustring value) { SS.GW.EditControlDone(value.c_str()); } private: GraphicsWidget _widget; EditorOverlay _overlay; Gtk::MenuBar _menubar; Gtk::VBox _box; bool _is_fullscreen; }; std::unique_ptr GW; void GetGraphicsWindowSize(int *w, int *h) { Gdk::Rectangle allocation = GW->get_widget().get_allocation(); *w = allocation.get_width(); *h = allocation.get_height(); } void InvalidateGraphics(void) { GW->get_widget().queue_draw(); } void PaintGraphics(void) { GW->get_widget().queue_draw(); /* Process animation */ Glib::MainContext::get_default()->iteration(false); } void SetCurrentFilename(const std::string &filename) { if(!filename.empty()) { GW->set_title("SolveSpace - " + filename); } else { GW->set_title("SolveSpace - (not yet saved)"); } } void ToggleFullScreen(void) { if(GW->is_fullscreen()) GW->unfullscreen(); else GW->fullscreen(); } bool FullScreenIsActive(void) { return GW->is_fullscreen(); } void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, const std::string &val) { Gdk::Rectangle rect = GW->get_widget().get_allocation(); // Convert to ij (vs. xy) style coordinates, // and compensate for the input widget height due to inverse coord int i, j; i = x + rect.get_width() / 2; j = -y + rect.get_height() / 2; GW->get_overlay().start_editing(i, j, fontHeight, /*is_monospace=*/false, minWidthChars, val); } void HideGraphicsEditControl(void) { GW->get_overlay().stop_editing(); } bool GraphicsEditControlIsVisible(void) { return GW->get_overlay().is_editing(); } /* TODO: removing menubar breaks accelerators. */ void ToggleMenuBar(void) { GW->get_menubar().set_visible(!GW->get_menubar().is_visible()); } bool MenuBarIsVisible(void) { return GW->get_menubar().is_visible(); } /* Context menus */ class ContextMenuItem : public Gtk::MenuItem { public: static ContextCommand choice; ContextMenuItem(const Glib::ustring &label, ContextCommand cmd, bool mnemonic=false) : Gtk::MenuItem(label, mnemonic), _cmd(cmd) { } protected: void on_activate() override { Gtk::MenuItem::on_activate(); if(has_submenu()) return; choice = _cmd; } /* Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=695488. This is used in addition to on_activate() to catch mouse events. Without on_activate(), it would be impossible to select a menu item via keyboard. This selects the item twice in some cases, but we are idempotent. */ bool on_button_press_event(GdkEventButton *event) override { if(event->button == 1 && event->type == GDK_BUTTON_PRESS) { on_activate(); return true; } return Gtk::MenuItem::on_button_press_event(event); } private: ContextCommand _cmd; }; ContextCommand ContextMenuItem::choice = ContextCommand::CANCELLED; static Gtk::Menu *context_menu = NULL, *context_submenu = NULL; void AddContextMenuItem(const char *label, ContextCommand cmd) { Gtk::MenuItem *menu_item; if(label) menu_item = new ContextMenuItem(label, cmd); else menu_item = new Gtk::SeparatorMenuItem(); if(cmd == ContextCommand::SUBMENU) { menu_item->set_submenu(*context_submenu); context_submenu = NULL; } if(context_submenu) { context_submenu->append(*menu_item); } else { if(!context_menu) context_menu = new Gtk::Menu; context_menu->append(*menu_item); } } void CreateContextSubmenu(void) { ssassert(!context_submenu, "Unexpected nested submenu"); context_submenu = new Gtk::Menu; } ContextCommand ShowContextMenu(void) { if(!context_menu) return ContextCommand::CANCELLED; Glib::RefPtr loop = Glib::MainLoop::create(); context_menu->signal_deactivate(). connect(sigc::mem_fun(loop.operator->(), &Glib::MainLoop::quit)); ContextMenuItem::choice = ContextCommand::CANCELLED; context_menu->show_all(); context_menu->popup(3, GDK_CURRENT_TIME); loop->run(); delete context_menu; context_menu = NULL; return ContextMenuItem::choice; } /* Main menu */ template class MainMenuItem : public MenuItem { public: MainMenuItem(const GraphicsWindow::MenuEntry &entry) : MenuItem(), _entry(entry), _synthetic(false) { Glib::ustring label(_entry.label); for(size_t i = 0; i < label.length(); i++) { if(label[i] == '&') label.replace(i, 1, "_"); } guint accel_key = 0; Gdk::ModifierType accel_mods = Gdk::ModifierType(); switch(_entry.accel) { case GraphicsWindow::DELETE_KEY: accel_key = GDK_KEY_Delete; break; case GraphicsWindow::ESCAPE_KEY: accel_key = GDK_KEY_Escape; break; case '\t': accel_key = GDK_KEY_Tab; break; default: accel_key = _entry.accel & ~(GraphicsWindow::SHIFT_MASK | GraphicsWindow::CTRL_MASK); if(accel_key > GraphicsWindow::FUNCTION_KEY_BASE && accel_key <= GraphicsWindow::FUNCTION_KEY_BASE + 12) accel_key = GDK_KEY_F1 + (accel_key - GraphicsWindow::FUNCTION_KEY_BASE - 1); else accel_key = gdk_unicode_to_keyval(accel_key); if(_entry.accel & GraphicsWindow::SHIFT_MASK) accel_mods |= Gdk::SHIFT_MASK; if(_entry.accel & GraphicsWindow::CTRL_MASK) accel_mods |= Gdk::CONTROL_MASK; } MenuItem::set_label(label); MenuItem::set_use_underline(true); if(!(accel_key & 0x01000000)) MenuItem::set_accel_key(Gtk::AccelKey(accel_key, accel_mods)); } void set_active(bool checked) { if(MenuItem::get_active() == checked) return; _synthetic = true; MenuItem::set_active(checked); } protected: void on_activate() override { MenuItem::on_activate(); if(_synthetic) _synthetic = false; else if(!MenuItem::has_submenu() && _entry.fn) _entry.fn(_entry.id); } private: const GraphicsWindow::MenuEntry &_entry; bool _synthetic; }; static std::map main_menu_items; static void InitMainMenu(Gtk::MenuShell *menu_shell) { Gtk::MenuItem *menu_item = NULL; Gtk::MenuShell *levels[5] = {menu_shell, 0}; const GraphicsWindow::MenuEntry *entry = &GraphicsWindow::menu[0]; int current_level = 0; while(entry->level >= 0) { if(entry->level > current_level) { Gtk::Menu *menu = new Gtk::Menu; menu_item->set_submenu(*menu); ssassert((unsigned)entry->level < sizeof(levels) / sizeof(levels[0]), "Unexpected depth of menu nesting"); levels[entry->level] = menu; } current_level = entry->level; if(entry->label) { switch(entry->kind) { case GraphicsWindow::MenuKind::NORMAL: menu_item = new MainMenuItem(*entry); break; case GraphicsWindow::MenuKind::CHECK: menu_item = new MainMenuItem(*entry); break; case GraphicsWindow::MenuKind::RADIO: MainMenuItem *radio_item = new MainMenuItem(*entry); radio_item->set_draw_as_radio(true); menu_item = radio_item; break; } } else { menu_item = new Gtk::SeparatorMenuItem(); } levels[entry->level]->append(*menu_item); main_menu_items[(uint32_t)entry->id] = menu_item; ++entry; } } void EnableMenuByCmd(Command cmd, bool enabled) { main_menu_items[(uint32_t)cmd]->set_sensitive(enabled); } void CheckMenuByCmd(Command cmd, bool checked) { ((MainMenuItem*)main_menu_items[(uint32_t)cmd])->set_active(checked); } void RadioMenuByCmd(Command cmd, bool selected) { SolveSpace::CheckMenuByCmd(cmd, selected); } class RecentMenuItem : public Gtk::MenuItem { public: RecentMenuItem(const Glib::ustring& label, uint32_t cmd) : MenuItem(label), _cmd(cmd) { } protected: void on_activate() override { if(_cmd >= (uint32_t)Command::RECENT_OPEN && _cmd < ((uint32_t)Command::RECENT_OPEN + MAX_RECENT)) { SolveSpaceUI::MenuFile((Command)_cmd); } else if(_cmd >= (uint32_t)Command::RECENT_LINK && _cmd < ((uint32_t)Command::RECENT_LINK + MAX_RECENT)) { Group::MenuGroup((Command)_cmd); } } private: uint32_t _cmd; }; static void RefreshRecentMenu(Command cmd, Command base) { Gtk::MenuItem *recent = static_cast(main_menu_items[(uint32_t)cmd]); recent->unset_submenu(); Gtk::Menu *menu = new Gtk::Menu; recent->set_submenu(*menu); if(std::string(RecentFile[0]).empty()) { Gtk::MenuItem *placeholder = new Gtk::MenuItem("(no recent files)"); placeholder->set_sensitive(false); menu->append(*placeholder); } else { for(size_t i = 0; i < MAX_RECENT; i++) { if(std::string(RecentFile[i]).empty()) break; RecentMenuItem *item = new RecentMenuItem(RecentFile[i], (uint32_t)base + i); menu->append(*item); } } menu->show_all(); } void RefreshRecentMenus(void) { RefreshRecentMenu(Command::OPEN_RECENT, Command::RECENT_OPEN); RefreshRecentMenu(Command::GROUP_RECENT, Command::RECENT_LINK); } /* Save/load */ static std::string ConvertFilters(std::string active, const FileFilter ssFilters[], Gtk::FileChooser *chooser) { for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { #ifdef HAVE_GTK3 Glib::RefPtr filter = Gtk::FileFilter::create(); #else Gtk::FileFilter *filter = new Gtk::FileFilter; #endif filter->set_name(ssFilter->name); bool is_active = false; std::string desc = ""; for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { std::string pattern = "*." + std::string(*ssPattern); filter->add_pattern(pattern); if(active == "") active = pattern.substr(2); if("*." + active == pattern) is_active = true; if(desc == "") desc = pattern; else desc += ", " + pattern; } filter->set_name(filter->get_name() + " (" + desc + ")"); #ifdef HAVE_GTK3 chooser->add_filter(filter); if(is_active) chooser->set_filter(filter); #else chooser->add_filter(*filter); if(is_active) chooser->set_filter(*filter); #endif } return active; } bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Open File"); chooser.set_filename(*filename); chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL); chooser.add_button("_Open", Gtk::RESPONSE_OK); chooser.set_current_folder(CnfThawString("", "FileChooserPath")); ConvertFilters(activeOrEmpty, filters, &chooser); if(chooser.run() == Gtk::RESPONSE_OK) { CnfFreezeString(chooser.get_current_folder(), "FileChooserPath"); *filename = chooser.get_filename(); return true; } else { return false; } } /* Glib::path_get_basename got /removed/ in 3.0?! Come on */ static std::string Basename(std::string filename) { int slash = filename.rfind('/'); if(slash >= 0) return filename.substr(slash + 1, filename.length()); return ""; } static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser) { /* Extract the pattern from the filter. GtkFileFilter doesn't provide any way to list the patterns, so we extract it from the filter name. Gross. */ std::string filter_name = chooser->get_filter()->get_name(); int lparen = filter_name.rfind('(') + 1; int rdelim = filter_name.find(',', lparen); if(rdelim < 0) rdelim = filter_name.find(')', lparen); ssassert(lparen > 0 && rdelim > 0, "Expected to find a parenthesized extension"); std::string extension = filter_name.substr(lparen, rdelim - lparen); if(extension == "*") return; if(extension.length() > 2 && extension.substr(0, 2) == "*.") extension = extension.substr(2, extension.length() - 2); std::string basename = Basename(chooser->get_filename()); int dot = basename.rfind('.'); if(dot >= 0) { basename.replace(dot + 1, basename.length() - dot - 1, extension); chooser->set_current_name(basename); } else { chooser->set_current_name(basename + "." + extension); } } bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Save File", Gtk::FILE_CHOOSER_ACTION_SAVE); chooser.set_do_overwrite_confirmation(true); chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL); chooser.add_button("_Save", Gtk::RESPONSE_OK); std::string active = ConvertFilters(activeOrEmpty, filters, &chooser); chooser.set_current_folder(CnfThawString("", "FileChooserPath")); chooser.set_current_name(std::string("untitled.") + active); /* Gtk's dialog doesn't change the extension when you change the filter, and makes it extremely hard to do so. Gtk is garbage. */ chooser.property_filter().signal_changed(). connect(sigc::bind(sigc::ptr_fun(&ChooserFilterChanged), &chooser)); if(chooser.run() == Gtk::RESPONSE_OK) { CnfFreezeString(chooser.get_current_folder(), "FileChooserPath"); *filename = chooser.get_filename(); return true; } else { return false; } } DialogChoice SaveFileYesNoCancel(void) { Glib::ustring message = "The file has changed since it was last saved.\n" "Do you want to save the changes?"; Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title("SolveSpace - Modified File"); dialog.add_button("_Save", Gtk::RESPONSE_YES); dialog.add_button("Do_n't Save", Gtk::RESPONSE_NO); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); switch(dialog.run()) { case Gtk::RESPONSE_YES: return DIALOG_YES; case Gtk::RESPONSE_NO: return DIALOG_NO; case Gtk::RESPONSE_CANCEL: default: return DIALOG_CANCEL; } } DialogChoice LoadAutosaveYesNo(void) { Glib::ustring message = "An autosave file is availible for this project.\n" "Do you want to load the autosave file instead?"; Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title("SolveSpace - Autosave Available"); dialog.add_button("_Load autosave", Gtk::RESPONSE_YES); dialog.add_button("Do_n't Load", Gtk::RESPONSE_NO); switch(dialog.run()) { case Gtk::RESPONSE_YES: return DIALOG_YES; case Gtk::RESPONSE_NO: default: return DIALOG_NO; } } DialogChoice LocateImportedFileYesNoCancel(const std::string &filename, bool canCancel) { Glib::ustring message = "The linked file " + filename + " is not present.\n" "Do you want to locate it manually?\n" "If you select \"No\", any geometry that depends on " "the missing file will be removed."; Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title("SolveSpace - Missing File"); dialog.add_button("_Yes", Gtk::RESPONSE_YES); dialog.add_button("_No", Gtk::RESPONSE_NO); if(canCancel) dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); switch(dialog.run()) { case Gtk::RESPONSE_YES: return DIALOG_YES; case Gtk::RESPONSE_NO: return DIALOG_NO; case Gtk::RESPONSE_CANCEL: default: return DIALOG_CANCEL; } } /* Text window */ class TextWidget : public GlWidget { public: #ifdef HAVE_GTK3 TextWidget(Glib::RefPtr adjustment) : _adjustment(adjustment) { #else TextWidget(Gtk::Adjustment* adjustment) : _adjustment(adjustment) { #endif set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::LEAVE_NOTIFY_MASK); } void set_cursor_hand(bool is_hand) { Glib::RefPtr gdkwin = get_window(); if(gdkwin) { // returns NULL if not realized Gdk::CursorType type = is_hand ? Gdk::HAND1 : Gdk::ARROW; #ifdef HAVE_GTK3 gdkwin->set_cursor(Gdk::Cursor::create(type)); #else gdkwin->set_cursor(Gdk::Cursor(type)); #endif } } protected: void on_gl_draw() override { SS.TW.Paint(); } bool on_motion_notify_event(GdkEventMotion *event) override { SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ event->state & GDK_BUTTON1_MASK, event->x, event->y); return true; } bool on_button_press_event(GdkEventButton *event) override { SS.TW.MouseEvent(/*leftClick*/ event->type == GDK_BUTTON_PRESS, /*leftDown*/ event->state & GDK_BUTTON1_MASK, event->x, event->y); return true; } bool on_scroll_event(GdkEventScroll *event) override { _adjustment->set_value(_adjustment->get_value() + DeltaYOfScrollEvent(event) * _adjustment->get_page_increment()); return true; } bool on_leave_notify_event (GdkEventCrossing *) override { SS.TW.MouseLeave(); return true; } private: #ifdef HAVE_GTK3 Glib::RefPtr _adjustment; #else Gtk::Adjustment *_adjustment; #endif }; class TextWindowGtk : public Gtk::Window { public: TextWindowGtk() : _scrollbar(), _widget(_scrollbar.get_adjustment()), _overlay(_widget), _box() { set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY); set_skip_taskbar_hint(true); set_skip_pager_hint(true); set_title("SolveSpace - Property Browser"); set_default_size(420, 300); _box.pack_start(_overlay, true, true); _box.pack_start(_scrollbar, false, true); add(_box); _scrollbar.get_adjustment()->signal_value_changed(). connect(sigc::mem_fun(this, &TextWindowGtk::on_scrollbar_value_changed)); _overlay.signal_editing_done(). connect(sigc::mem_fun(this, &TextWindowGtk::on_editing_done)); _overlay.get_entry().signal_motion_notify_event(). connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_motion_notify_event)); _overlay.get_entry().signal_button_press_event(). connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_button_press_event)); } Gtk::VScrollbar &get_scrollbar() { return _scrollbar; } TextWidget &get_widget() { return _widget; } EditorOverlay &get_overlay() { return _overlay; } protected: void on_show() override { Gtk::Window::on_show(); CnfThawWindowPos(this, "TextWindow"); } void on_hide() override { CnfFreezeWindowPos(this, "TextWindow"); Gtk::Window::on_hide(); } bool on_key_press_event(GdkEventKey *event) override { if(GW->emulate_key_press(event)) { return true; } return Gtk::Window::on_key_press_event(event); } bool on_delete_event(GdkEventAny *) override { /* trigger the action and ignore the request */ GraphicsWindow::MenuView(Command::SHOW_TEXT_WND); return false; } void on_scrollbar_value_changed() { SS.TW.ScrollbarEvent((int)_scrollbar.get_adjustment()->get_value()); } void on_editing_done(Glib::ustring value) { SS.TW.EditControlDone(value.c_str()); } bool on_editor_motion_notify_event(GdkEventMotion *event) { return _widget.event((GdkEvent*) event); } bool on_editor_button_press_event(GdkEventButton *event) { return _widget.event((GdkEvent*) event); } private: Gtk::VScrollbar _scrollbar; TextWidget _widget; EditorOverlay _overlay; Gtk::HBox _box; }; std::unique_ptr TW; void ShowTextWindow(bool visible) { if(visible) TW->show(); else TW->hide(); } void GetTextWindowSize(int *w, int *h) { Gdk::Rectangle allocation = TW->get_widget().get_allocation(); *w = allocation.get_width(); *h = allocation.get_height(); } void InvalidateText(void) { TW->get_widget().queue_draw(); } void MoveTextScrollbarTo(int pos, int maxPos, int page) { TW->get_scrollbar().get_adjustment()->configure(pos, 0, maxPos, 1, 10, page); } void SetMousePointerToHand(bool is_hand) { TW->get_widget().set_cursor_hand(is_hand); } void ShowTextEditControl(int x, int y, const std::string &val) { TW->get_overlay().start_editing(x, y, TextWindow::CHAR_HEIGHT, /*is_monospace=*/true, 30, val); } void HideTextEditControl(void) { TW->get_overlay().stop_editing(); } bool TextEditControlIsVisible(void) { return TW->get_overlay().is_editing(); } /* Miscellanea */ void DoMessageBox(const char *message, int rows, int cols, bool error) { Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, error ? Gtk::MESSAGE_ERROR : Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, /*is_modal*/ true); dialog.set_title(error ? "SolveSpace - Error" : "SolveSpace - Message"); dialog.run(); } void OpenWebsite(const char *url) { gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL); } /* fontconfig is already initialized by GTK */ std::vector GetFontFiles() { std::vector fonts; FcPattern *pat = FcPatternCreate(); FcObjectSet *os = FcObjectSetBuild(FC_FILE, (char *)0); FcFontSet *fs = FcFontList(0, pat, os); for(int i = 0; i < fs->nfont; i++) { FcChar8 *filenameFC = FcPatternFormat(fs->fonts[i], (const FcChar8*) "%{file}"); std::string filename = (char*) filenameFC; fonts.push_back(filename); FcStrFree(filenameFC); } FcFontSetDestroy(fs); FcObjectSetDestroy(os); FcPatternDestroy(pat); return fonts; } static std::string resource_dir; const void *LoadResource(const std::string &name, size_t *size) { static std::map> cache; auto it = cache.find(name); if(it == cache.end()) { struct stat st; std::string path; path = (UNIX_DATADIR "/") + name; if(stat(path.c_str(), &st)) { ssassert(errno == ENOENT, "Unexpected stat() error"); path = resource_dir + "/" + name; ssassert(!stat(path.c_str(), &st), "Cannot find resource"); } std::vector data(st.st_size); FILE *f = ssfopen(path.c_str(), "rb"); ssassert(f != NULL, "Cannot open resource"); fread(&data[0], 1, st.st_size, f); fclose(f); cache.emplace(name, std::move(data)); it = cache.find(name); } *size = (*it).second.size(); return &(*it).second[0]; } /* Space Navigator support */ #ifdef HAVE_SPACEWARE static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *, gpointer) { XEvent *xevent = (XEvent*) gxevent; spnav_event sev; if(!spnav_x11_event(xevent, &sev)) return GDK_FILTER_CONTINUE; switch(sev.type) { case SPNAV_EVENT_MOTION: SS.GW.SpaceNavigatorMoved( (double)sev.motion.x, (double)sev.motion.y, (double)sev.motion.z * -1.0, (double)sev.motion.rx * 0.001, (double)sev.motion.ry * 0.001, (double)sev.motion.rz * -0.001, xevent->xmotion.state & ShiftMask); break; case SPNAV_EVENT_BUTTON: if(!sev.button.press && sev.button.bnum == SI_APP_FIT_BUTTON) { SS.GW.SpaceNavigatorButtonUp(); } break; } return GDK_FILTER_REMOVE; } #endif /* Application lifecycle */ void ExitNow(void) { GW->hide(); TW->hide(); } }; int main(int argc, char** argv) { /* It would in principle be possible to judiciously use Glib::filename_{from,to}_utf8, but it's not really worth the effort. The setlocale() call is necessary for Glib::get_charset() to detect the system character set; otherwise it thinks it is always ANSI_X3.4-1968. We set it back to C after all. */ setlocale(LC_ALL, ""); if(!Glib::get_charset()) { std::cerr << "Sorry, only UTF-8 locales are supported." << std::endl; return 1; } setlocale(LC_ALL, "C"); /* If we don't do this, gtk_init will set the C standard library locale, and printf will format floats using ",". We will then fail to parse these. Also, many text window lines will become ambiguous. */ gtk_disable_setlocale(); resource_dir = argv[0]; // .../src/solvespace resource_dir.erase(resource_dir.rfind('/')); resource_dir.erase(resource_dir.rfind('/')); resource_dir += "/res"; // .../res Gtk::Main main(argc, argv); #ifdef HAVE_SPACEWARE gdk_window_add_filter(NULL, GdkSpnavFilter, NULL); #endif CnfLoad(); SolveSpace::Pixmap icon = LoadPNG("freedesktop/solvespace-48x48.png"); Glib::RefPtr icon_gdk = Gdk::Pixbuf::create_from_data(&icon.data[0], Gdk::COLORSPACE_RGB, icon.hasAlpha, 8, icon.width, icon.height, icon.stride); TW.reset(new TextWindowGtk); GW.reset(new GraphicsWindowGtk); InitMainMenu(&GW->get_menubar()); GW->get_menubar().accelerate(*TW); TW->set_transient_for(*GW); GW->set_icon(icon_gdk); TW->set_icon(icon_gdk); TW->show_all(); GW->show_all(); SS.Init(); if(argc >= 2) { if(argc > 2) { std::cerr << "Only the first file passed on command line will be opened." << std::endl; } /* Make sure the argument is valid UTF-8. */ SS.OpenFile(Glib::ustring(argv[1])); } main.run(*GW); TW.reset(); GW.reset(); SK.Clear(); SS.Clear(); return 0; }