solvespace/src/platform/gtkmain.cpp
2017-01-14 02:41:23 +00:00

1478 lines
42 KiB
C++

//-----------------------------------------------------------------------------
// 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 <whitequark@whitequark.org>
//-----------------------------------------------------------------------------
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <json-c/json_object.h>
#include <json-c/json_util.h>
#include <glibmm/main.h>
#include <glibmm/convert.h>
#include <giomm/file.h>
#include <gdkmm/cursor.h>
#include <gtkmm/drawingarea.h>
#include <gtkmm/glarea.h>
#include <gtkmm/scrollbar.h>
#include <gtkmm/entry.h>
#include <gtkmm/eventbox.h>
#include <gtkmm/hvbox.h>
#include <gtkmm/fixed.h>
#include <gtkmm/adjustment.h>
#include <gtkmm/separatormenuitem.h>
#include <gtkmm/menuitem.h>
#include <gtkmm/checkmenuitem.h>
#include <gtkmm/radiomenuitem.h>
#include <gtkmm/radiobuttongroup.h>
#include <gtkmm/menu.h>
#include <gtkmm/menubar.h>
#include <gtkmm/filechooserdialog.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/main.h>
#include <cairomm/xlib_surface.h>
#include <pangomm/fontdescription.h>
#include <gdk/gdkx.h>
#include <fontconfig/fontconfig.h>
#include <GL/glx.h>
#include "solvespace.h"
#include "config.h"
#ifdef HAVE_SPACEWARE
#include <spnav.h>
#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<Pango::Layout> 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();
_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<void, Glib::ustring> 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<void, Glib::ustring> _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_alpha(true);
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<Gdk::GLContext> 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<Gdk::GLContext> &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;
}
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;
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<GraphicsWindowGtk> 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<Glib::MainLoop> 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 MenuItem> 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<uint32_t, Gtk::MenuItem *> 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<Gtk::MenuItem>(localizedEntry);
break;
case GraphicsWindow::MenuKind::CHECK:
menu_item = new MainMenuItem<Gtk::CheckMenuItem>(localizedEntry);
break;
case GraphicsWindow::MenuKind::RADIO:
MainMenuItem<Gtk::CheckMenuItem> *radio_item =
new MainMenuItem<Gtk::CheckMenuItem>(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<Gtk::MenuItem>(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<Gtk::CheckMenuItem>*)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<Gtk::MenuItem*>(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<Gtk::FileFilter> 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<Gtk::Adjustment> adjustment) : _adjustment(adjustment) {
set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK |
Gdk::LEAVE_NOTIFY_MASK);
set_has_alpha(true);
set_has_depth_buffer(true);
}
void set_cursor_hand(bool is_hand) {
Glib::RefPtr<Gdk::Window> 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<Gdk::GLContext> on_create_context() override {
return get_window()->create_gl_context();
}
bool on_render(const Glib::RefPtr<Gdk::GLContext> &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<Gtk::Adjustment> _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_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<TextWindowGtk> 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<std::string> GetFontFiles() {
std::vector<std::string> 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;
}