//----------------------------------------------------------------------------- // 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 #include #include #include #include #include #include "solvespace.h" #include "config.h" #ifdef HAVE_SPACEWARE #include #endif namespace SolveSpace { /* Utility functions */ std::string Title(const std::string &s) { return "SolveSpace - " + s; } /* 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 */ 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); } /* 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); _entry.override_font(font_desc); /* 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(); 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); _entry.set_text(val); if(!_entry.is_visible()) { _entry.show(); _entry.grab_focus(); add_modal_grab(); } } void stop_editing() { if(_entry.is_visible()) { 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(is_editing()) { if(event->keyval == GDK_KEY_Escape) { stop_editing(); } else { _entry.event((GdkEvent *)event); } return true; } else { return false; } } bool on_key_release_event(GdkEventKey *event) override { if(is_editing()) { _entry.event((GdkEvent *)event); return true; } else { 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) { double delta_y = event->delta_y; 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 Gtk::GLArea { 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_has_depth_buffer(true); } protected: // Work around a bug fixed in GTKMM 3.22: // https://mail.gnome.org/archives/gtkmm-list/2016-April/msg00020.html Glib::RefPtr on_create_context() override { return get_window()->create_gl_context(); } void on_resize(int width, int height) override { _w = width; _h = height; } bool on_render(const Glib::RefPtr &context) override { SS.GW.Paint(); return true; } 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 * get_scale_factor()) - _w / 2; y = _h / 2 - (int)(j * get_scale_factor()); } }; 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; } 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; case GDK_KEY_KP_Decimal: chr = '.'; 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() * GW->get_scale_factor(); *h = allocation.get_height() * GW->get_scale_factor(); } 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) { GW->set_title(Title(filename.empty() ? C_("title", "(new sketch)") : filename.c_str())); } 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) { GraphicsWindow::MenuEntry localizedEntry = *entry; localizedEntry.label = Translate(entry->label).c_str(); switch(entry->kind) { case GraphicsWindow::MenuKind::NORMAL: menu_item = new MainMenuItem(localizedEntry); break; case GraphicsWindow::MenuKind::CHECK: menu_item = new MainMenuItem(localizedEntry); break; case GraphicsWindow::MenuKind::RADIO: MainMenuItem *radio_item = new MainMenuItem(localizedEntry); radio_item->set_draw_as_radio(true); menu_item = radio_item; break; } } else { menu_item = new Gtk::SeparatorMenuItem(); } if(entry->id == Command::LOCALE) { Gtk::Menu *menu = new Gtk::Menu; menu_item->set_submenu(*menu); size_t i = 0; for(auto locale : Locales()) { GraphicsWindow::MenuEntry localeEntry = {}; localeEntry.label = locale.displayName.c_str(); localeEntry.id = (Command)((uint32_t)Command::LOCALE + i++); localeEntry.fn = entry->fn; menu->append(*new MainMenuItem(localeEntry)); } } 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(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(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++) { Glib::RefPtr filter = Gtk::FileFilter::create(); filter->set_name(Translate(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); filter->add_pattern(Glib::ustring(pattern).uppercase()); 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 + ")"); chooser->add_filter(filter); if(is_active) chooser->set_filter(filter); } return active; } bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "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; } } 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 &defExtension, const FileFilter filters[]) { Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Save File")), Gtk::FILE_CHOOSER_ACTION_SAVE); chooser.set_do_overwrite_confirmation(true); chooser.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); chooser.add_button(C_("button", "_Save"), Gtk::RESPONSE_OK); std::string activeExtension = ConvertFilters(defExtension, filters, &chooser); if(filename->empty()) { chooser.set_current_folder(CnfThawString("", "FileChooserPath")); chooser.set_current_name(std::string(_("untitled")) + "." + activeExtension); } else { chooser.set_current_folder(Dirname(*filename)); chooser.set_current_name(Basename(*filename, /*stripExtension=*/true) + "." + activeExtension); } /* 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\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(Title(C_("title", "Modified File"))); dialog.add_button(C_("button", "_Save"), Gtk::RESPONSE_YES); dialog.add_button(C_("button", "Do_n't Save"), Gtk::RESPONSE_NO); dialog.add_button(C_("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\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(Title(C_("title", "Autosave Available"))); dialog.add_button(C_("button", "_Load autosave"), Gtk::RESPONSE_YES); dialog.add_button(C_("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\n" "Do you want to locate it manually?\n\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(Title(C_("title", "Missing File"))); dialog.add_button(C_("button", "_Yes"), Gtk::RESPONSE_YES); dialog.add_button(C_("button", "_No"), Gtk::RESPONSE_NO); if(canCancel) dialog.add_button(C_("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 Gtk::GLArea { public: TextWidget(Glib::RefPtr adjustment) : _adjustment(adjustment) { set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::LEAVE_NOTIFY_MASK); set_has_depth_buffer(true); } 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; gdkwin->set_cursor(Gdk::Cursor::create(type)); } } protected: // See GraphicsWidget::on_create_context. Glib::RefPtr on_create_context() override { return get_window()->create_gl_context(); } bool on_render(const Glib::RefPtr &context) override { SS.TW.Paint(); return true; } bool on_motion_notify_event(GdkEventMotion *event) override { SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ event->state & GDK_BUTTON1_MASK, event->x * get_scale_factor(), event->y * get_scale_factor()); 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 * get_scale_factor(), event->y * get_scale_factor()); 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: Glib::RefPtr _adjustment; }; 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_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_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() * TW->get_scale_factor(); *h = allocation.get_height() * TW->get_scale_factor(); } double GetScreenDpi() { return Gdk::Screen::get_default()->get_resolution(); } 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 ? Title(C_("title", "Error")) : Title(C_("title", "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; } /* 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 == 0) { SS.GW.SpaceNavigatorButtonUp(); } break; } return GDK_FILTER_REMOVE; } #endif /* Application lifecycle */ void RefreshLocale() { SS.UpdateWindowTitle(); for(auto menu : GW->get_menubar().get_children()) { GW->get_menubar().remove(*menu); } InitMainMenu(&GW->get_menubar()); RefreshRecentMenus(); GW->get_menubar().show_all(); GW->get_menubar().accelerate(*GW); GW->get_menubar().accelerate(*TW); TW->set_title(Title(C_("title", "Property Browser"))); } void ExitNow() { 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()) { dbp("Sorry, only UTF-8 locales are supported."); 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(); Gtk::Main main(argc, argv); #ifdef HAVE_SPACEWARE gdk_window_add_filter(NULL, GdkSpnavFilter, NULL); #endif CnfLoad(); auto icon = LoadPng("freedesktop/solvespace-48x48.png"); auto icon_gdk = Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB, icon->format == SolveSpace::Pixmap::Format::RGBA, 8, icon->width, icon->height, icon->stride); TW.reset(new TextWindowGtk); GW.reset(new GraphicsWindowGtk); TW->set_transient_for(*GW); GW->set_icon(icon_gdk); TW->set_icon(icon_gdk); TW->show_all(); GW->show_all(); const char* const* langNames = g_get_language_names(); while(*langNames) { if(SetLocale(*langNames++)) break; } if(!*langNames) { SetLocale("en_US"); } #if defined(HAVE_SPACEWARE) && defined(GDK_WINDOWING_X11) if(GDK_IS_X11_DISPLAY(Gdk::Display::get_default()->gobj())) { // We don't care if it can't be opened; just continue without. spnav_x11_open(gdk_x11_get_default_xdisplay(), gdk_x11_window_get_xid(GW->get_window()->gobj())); } #endif SS.Init(); if(argc >= 2) { if(argc > 2) { dbp("Only the first file passed on command line will be opened."); } /* Make sure the argument is valid UTF-8. */ SS.OpenFile(PathFromCurrentDirectory(Glib::ustring(argv[1]))); } main.run(*GW); TW.reset(); GW.reset(); SK.Clear(); SS.Clear(); return 0; }