solvespace/src/platform/guigtk.cpp

995 lines
31 KiB
C++
Raw Normal View History

//-----------------------------------------------------------------------------
// The GTK-based implementation of platform-dependent GUI functionality.
//
// Copyright 2018 whitequark
//-----------------------------------------------------------------------------
#include "solvespace.h"
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <json-c/json_object.h>
#include <json-c/json_util.h>
#include <glibmm/main.h>
#include <gtkmm/box.h>
#include <gtkmm/checkmenuitem.h>
#include <gtkmm/entry.h>
#include <gtkmm/fixed.h>
#include <gtkmm/glarea.h>
#include <gtkmm/main.h>
#include <gtkmm/menu.h>
#include <gtkmm/menubar.h>
#include <gtkmm/scrollbar.h>
#include <gtkmm/separatormenuitem.h>
#include <gtkmm/window.h>
namespace SolveSpace {
namespace Platform {
//-----------------------------------------------------------------------------
// Fatal errors
//-----------------------------------------------------------------------------
void FatalError(std::string message) {
fprintf(stderr, "%s", message.c_str());
abort();
}
//-----------------------------------------------------------------------------
// Settings
//-----------------------------------------------------------------------------
class SettingsImplGtk : public Settings {
public:
// Why aren't we using 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.
Path _path;
json_object *_json = NULL;
static Path GetConfigPath() {
Path configHome;
if(getenv("XDG_CONFIG_HOME")) {
configHome = Path::From(getenv("XDG_CONFIG_HOME"));
} else if(getenv("HOME")) {
configHome = Path::From(getenv("HOME")).Join(".config");
} else {
dbp("neither XDG_CONFIG_HOME nor HOME are set");
return Path::From("");
}
if(!configHome.IsEmpty()) {
configHome = configHome.Join("solvespace");
}
const char *configHomeC = configHome.raw.c_str();
struct stat st;
if(stat(configHomeC, &st)) {
if(errno == ENOENT) {
if(mkdir(configHomeC, 0777)) {
dbp("cannot mkdir %s: %s", configHomeC, strerror(errno));
return Path::From("");
}
} else {
dbp("cannot stat %s: %s", configHomeC, strerror(errno));
return Path::From("");
}
} else if(!S_ISDIR(st.st_mode)) {
dbp("%s is not a directory", configHomeC);
return Path::From("");
}
return configHome.Join("settings.json");
}
SettingsImplGtk() {
_path = GetConfigPath();
if(_path.IsEmpty()) {
dbp("settings will not be saved");
} else {
_json = json_object_from_file(_path.raw.c_str());
if(!_json && errno != ENOENT) {
dbp("cannot load settings: %s", strerror(errno));
}
}
if(_json == NULL) {
_json = json_object_new_object();
}
}
~SettingsImplGtk() {
if(!_path.IsEmpty()) {
// json-c <0.12 has the first argument non-const
if(json_object_to_file_ext((char *)_path.raw.c_str(), _json,
JSON_C_TO_STRING_PRETTY)) {
dbp("cannot save settings: %s", strerror(errno));
}
}
json_object_put(_json);
}
void FreezeInt(const std::string &key, uint32_t value) override {
struct json_object *jsonValue = json_object_new_int(value);
json_object_object_add(_json, key.c_str(), jsonValue);
}
uint32_t ThawInt(const std::string &key, uint32_t defaultValue) override {
struct json_object *jsonValue;
if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) {
return json_object_get_int(jsonValue);
}
return defaultValue;
}
void FreezeBool(const std::string &key, bool value) override {
struct json_object *jsonValue = json_object_new_boolean(value);
json_object_object_add(_json, key.c_str(), jsonValue);
}
bool ThawBool(const std::string &key, bool defaultValue) override {
struct json_object *jsonValue;
if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) {
return json_object_get_boolean(jsonValue);
}
return defaultValue;
}
void FreezeFloat(const std::string &key, double value) override {
struct json_object *jsonValue = json_object_new_double(value);
json_object_object_add(_json, key.c_str(), jsonValue);
}
double ThawFloat(const std::string &key, double defaultValue) override {
struct json_object *jsonValue;
if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) {
return json_object_get_double(jsonValue);
}
return defaultValue;
}
void FreezeString(const std::string &key, const std::string &value) override {
struct json_object *jsonValue = json_object_new_string(value.c_str());
json_object_object_add(_json, key.c_str(), jsonValue);
}
std::string ThawString(const std::string &key,
const std::string &defaultValue = "") override {
struct json_object *jsonValue;
if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) {
return json_object_get_string(jsonValue);
}
return defaultValue;
}
};
SettingsRef GetSettings() {
static std::shared_ptr<SettingsImplGtk> settings;
if(!settings) {
settings = std::make_shared<SettingsImplGtk>();
}
return settings;
}
//-----------------------------------------------------------------------------
// Timers
//-----------------------------------------------------------------------------
class TimerImplGtk : public Timer {
public:
sigc::connection _connection;
void WindUp(unsigned milliseconds) override {
if(!_connection.empty()) {
_connection.disconnect();
}
auto handler = [this]() {
if(this->onTimeout) {
this->onTimeout();
}
return false;
};
_connection = Glib::signal_timeout().connect(handler, milliseconds);
}
};
TimerRef CreateTimer() {
return std::unique_ptr<TimerImplGtk>(new TimerImplGtk);
}
//-----------------------------------------------------------------------------
// GTK menu extensions
//-----------------------------------------------------------------------------
class GtkMenuItem : public Gtk::CheckMenuItem {
Platform::MenuItem *_receiver;
bool _has_indicator;
bool _synthetic_event;
public:
GtkMenuItem(Platform::MenuItem *receiver) :
_receiver(receiver), _has_indicator(false), _synthetic_event(false) {
}
void set_accel_key(const Gtk::AccelKey &accel_key) {
Gtk::CheckMenuItem::set_accel_key(accel_key);
}
bool has_indicator() const {
return _has_indicator;
}
void set_has_indicator(bool has_indicator) {
_has_indicator = has_indicator;
}
void set_active(bool active) {
if(Gtk::CheckMenuItem::get_active() == active)
return;
_synthetic_event = true;
Gtk::CheckMenuItem::set_active(active);
_synthetic_event = false;
}
protected:
void on_activate() override {
Gtk::CheckMenuItem::on_activate();
if(!_synthetic_event && _receiver->onTrigger) {
_receiver->onTrigger();
}
}
void draw_indicator_vfunc(const Cairo::RefPtr<Cairo::Context> &cr) override {
if(_has_indicator) {
Gtk::CheckMenuItem::draw_indicator_vfunc(cr);
}
}
};
//-----------------------------------------------------------------------------
// Menus
//-----------------------------------------------------------------------------
static std::string PrepareMenuLabel(std::string label) {
std::replace(label.begin(), label.end(), '&', '_');
return label;
}
class MenuItemImplGtk : public MenuItem {
public:
GtkMenuItem gtkMenuItem;
MenuItemImplGtk() : gtkMenuItem(this) {}
void SetAccelerator(KeyboardEvent accel) override {
guint accelKey;
if(accel.key == KeyboardEvent::Key::CHARACTER) {
if(accel.chr == '\t') {
accelKey = GDK_KEY_Tab;
} else if(accel.chr == '\x1b') {
accelKey = GDK_KEY_Escape;
} else if(accel.chr == '\x7f') {
accelKey = GDK_KEY_Delete;
} else {
accelKey = gdk_unicode_to_keyval(accel.chr);
}
} else if(accel.key == KeyboardEvent::Key::FUNCTION) {
accelKey = GDK_KEY_F1 + accel.num - 1;
}
Gdk::ModifierType accelMods = {};
if(accel.shiftDown) {
accelMods |= Gdk::SHIFT_MASK;
}
if(accel.controlDown) {
accelMods |= Gdk::CONTROL_MASK;
}
gtkMenuItem.set_accel_key(Gtk::AccelKey(accelKey, accelMods));
}
void SetIndicator(Indicator type) override {
switch(type) {
case Indicator::NONE:
gtkMenuItem.set_has_indicator(false);
break;
case Indicator::CHECK_MARK:
gtkMenuItem.set_has_indicator(true);
gtkMenuItem.set_draw_as_radio(false);
break;
case Indicator::RADIO_MARK:
gtkMenuItem.set_has_indicator(true);
gtkMenuItem.set_draw_as_radio(true);
break;
}
}
void SetActive(bool active) override {
ssassert(gtkMenuItem.has_indicator(),
"Cannot change state of a menu item without indicator");
gtkMenuItem.set_active(active);
}
void SetEnabled(bool enabled) override {
gtkMenuItem.set_sensitive(enabled);
}
};
class MenuImplGtk : public Menu {
public:
Gtk::Menu gtkMenu;
std::vector<std::shared_ptr<MenuItemImplGtk>> menuItems;
std::vector<std::shared_ptr<MenuImplGtk>> subMenus;
MenuItemRef AddItem(const std::string &label,
std::function<void()> onTrigger = NULL) override {
auto menuItem = std::make_shared<MenuItemImplGtk>();
menuItems.push_back(menuItem);
menuItem->gtkMenuItem.set_label(PrepareMenuLabel(label));
menuItem->gtkMenuItem.set_use_underline(true);
menuItem->gtkMenuItem.show();
menuItem->onTrigger = onTrigger;
gtkMenu.append(menuItem->gtkMenuItem);
return menuItem;
}
MenuRef AddSubMenu(const std::string &label) override {
auto menuItem = std::make_shared<MenuItemImplGtk>();
menuItems.push_back(menuItem);
auto subMenu = std::make_shared<MenuImplGtk>();
subMenus.push_back(subMenu);
menuItem->gtkMenuItem.set_label(PrepareMenuLabel(label));
menuItem->gtkMenuItem.set_use_underline(true);
menuItem->gtkMenuItem.set_submenu(subMenu->gtkMenu);
menuItem->gtkMenuItem.show_all();
gtkMenu.append(menuItem->gtkMenuItem);
return subMenu;
}
void AddSeparator() override {
Gtk::SeparatorMenuItem *gtkMenuItem = Gtk::manage(new Gtk::SeparatorMenuItem());
gtkMenuItem->show();
gtkMenu.append(*Gtk::manage(gtkMenuItem));
}
void PopUp() override {
Glib::RefPtr<Glib::MainLoop> loop = Glib::MainLoop::create();
auto signal = gtkMenu.signal_deactivate().connect([&]() { loop->quit(); });
gtkMenu.show_all();
gtkMenu.popup(0, GDK_CURRENT_TIME);
loop->run();
signal.disconnect();
}
void Clear() override {
gtkMenu.foreach([&](Gtk::Widget &w) { gtkMenu.remove(w); });
menuItems.clear();
subMenus.clear();
}
};
MenuRef CreateMenu() {
return std::make_shared<MenuImplGtk>();
}
class MenuBarImplGtk : public MenuBar {
public:
Gtk::MenuBar gtkMenuBar;
std::vector<std::shared_ptr<MenuImplGtk>> subMenus;
MenuRef AddSubMenu(const std::string &label) override {
auto subMenu = std::make_shared<MenuImplGtk>();
subMenus.push_back(subMenu);
Gtk::MenuItem *gtkMenuItem = Gtk::manage(new Gtk::MenuItem);
gtkMenuItem->set_label(PrepareMenuLabel(label));
gtkMenuItem->set_use_underline(true);
gtkMenuItem->set_submenu(subMenu->gtkMenu);
gtkMenuItem->show_all();
gtkMenuBar.append(*gtkMenuItem);
return subMenu;
}
void Clear() override {
gtkMenuBar.foreach([&](Gtk::Widget &w) { gtkMenuBar.remove(w); });
subMenus.clear();
}
};
MenuBarRef GetOrCreateMainMenu(bool *unique) {
*unique = false;
return std::make_shared<MenuBarImplGtk>();
}
//-----------------------------------------------------------------------------
// GTK GL and window extensions
//-----------------------------------------------------------------------------
class GtkGLWidget : public Gtk::GLArea {
Window *_receiver;
public:
GtkGLWidget(Platform::Window *receiver) : _receiver(receiver) {
set_has_depth_buffer(true);
set_can_focus(true);
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 |
Gdk::KEY_PRESS_MASK |
Gdk::KEY_RELEASE_MASK);
}
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();
}
bool on_render(const Glib::RefPtr<Gdk::GLContext> &context) override {
if(_receiver->onRender) {
_receiver->onRender();
}
return true;
}
bool process_pointer_event(MouseEvent::Type type, double x, double y,
guint state, guint button = 0, int scroll_delta = 0) {
MouseEvent event = {};
event.type = type;
event.x = x;
event.y = y;
if(button == 1 || (state & GDK_BUTTON1_MASK) != 0) {
event.button = MouseEvent::Button::LEFT;
} else if(button == 2 || (state & GDK_BUTTON2_MASK) != 0) {
event.button = MouseEvent::Button::MIDDLE;
} else if(button == 3 || (state & GDK_BUTTON3_MASK) != 0) {
event.button = MouseEvent::Button::RIGHT;
}
if((state & GDK_SHIFT_MASK) != 0) {
event.shiftDown = true;
}
if((state & GDK_CONTROL_MASK) != 0) {
event.controlDown = true;
}
if(scroll_delta != 0) {
event.scrollDelta = scroll_delta;
}
if(_receiver->onMouseEvent) {
return _receiver->onMouseEvent(event);
}
return false;
}
bool on_motion_notify_event(GdkEventMotion *gdk_event) override {
if(process_pointer_event(MouseEvent::Type::MOTION,
gdk_event->x, gdk_event->y, gdk_event->state))
return true;
return Gtk::GLArea::on_motion_notify_event(gdk_event);
}
bool on_button_press_event(GdkEventButton *gdk_event) override {
MouseEvent::Type type;
if(gdk_event->type == GDK_BUTTON_PRESS) {
type = MouseEvent::Type::PRESS;
} else if(gdk_event->type == GDK_2BUTTON_PRESS) {
type = MouseEvent::Type::DBL_PRESS;
} else {
return Gtk::GLArea::on_button_press_event(gdk_event);
}
if(process_pointer_event(type, gdk_event->x, gdk_event->y,
gdk_event->state, gdk_event->button))
return true;
return Gtk::GLArea::on_button_press_event(gdk_event);
}
bool on_button_release_event(GdkEventButton *gdk_event) override {
if(process_pointer_event(MouseEvent::Type::RELEASE,
gdk_event->x, gdk_event->y,
gdk_event->state, gdk_event->button))
return true;
return Gtk::GLArea::on_button_release_event(gdk_event);
}
bool on_scroll_event(GdkEventScroll *gdk_event) override {
int delta;
if(gdk_event->delta_y < 0 || gdk_event->direction == GDK_SCROLL_UP) {
delta = 1;
} else if(gdk_event->delta_y > 0 || gdk_event->direction == GDK_SCROLL_DOWN) {
delta = -1;
} else {
return false;
}
if(process_pointer_event(MouseEvent::Type::SCROLL_VERT,
gdk_event->x, gdk_event->y,
gdk_event->state, 0, delta))
return true;
return Gtk::GLArea::on_scroll_event(gdk_event);
}
bool on_leave_notify_event(GdkEventCrossing *gdk_event) override {
if(process_pointer_event(MouseEvent::Type::LEAVE,
gdk_event->x, gdk_event->y, gdk_event->state))
return true;
return Gtk::GLArea::on_leave_notify_event(gdk_event);
}
bool process_key_event(KeyboardEvent::Type type, GdkEventKey *gdk_event) {
KeyboardEvent event = {};
event.type = type;
if(gdk_event->state & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
return false;
}
event.shiftDown = (gdk_event->state & GDK_SHIFT_MASK) != 0;
event.controlDown = (gdk_event->state & GDK_CONTROL_MASK) != 0;
char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(gdk_event->keyval));
if(chr != 0) {
event.key = KeyboardEvent::Key::CHARACTER;
event.chr = chr;
} else if(gdk_event->keyval >= GDK_KEY_F1 &&
gdk_event->keyval <= GDK_KEY_F12) {
event.key = KeyboardEvent::Key::FUNCTION;
event.num = gdk_event->keyval - GDK_KEY_F1 + 1;
} else {
return false;
}
if(SS.GW.KeyboardEvent(event)) {
return true;
}
return false;
}
bool on_key_press_event(GdkEventKey *gdk_event) override {
if(process_key_event(KeyboardEvent::Type::PRESS, gdk_event))
return true;
return Gtk::GLArea::on_key_press_event(gdk_event);
}
bool on_key_release_event(GdkEventKey *gdk_event) override {
if(process_key_event(KeyboardEvent::Type::RELEASE, gdk_event))
return true;
return Gtk::GLArea::on_key_release_event(gdk_event);
}
};
class GtkEditorOverlay : public Gtk::Fixed {
Window *_receiver;
GtkGLWidget _gl_widget;
Gtk::Entry _entry;
public:
GtkEditorOverlay(Platform::Window *receiver) : _receiver(receiver), _gl_widget(receiver) {
add(_gl_widget);
_entry.set_no_show_all(true);
_entry.set_has_frame(false);
add(_entry);
_entry.signal_activate().
connect(sigc::mem_fun(this, &GtkEditorOverlay::on_activate));
}
bool is_editing() const {
return _entry.is_visible();
}
void start_editing(int x, int y, int font_height, int min_width, bool is_monospace,
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);
// The 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);
// Add one extra char width to avoid scrolling.
layout->set_text(val + " ");
int width = layout->get_logical_extents().get_width();
Gtk::Border margin = _entry.get_style_context()->get_margin();
Gtk::Border border = _entry.get_style_context()->get_border();
Gtk::Border padding = _entry.get_style_context()->get_padding();
move(_entry,
x - margin.get_left() - border.get_left() - padding.get_left(),
y - margin.get_top() - border.get_top() - padding.get_top());
int fitWidth = width / Pango::SCALE + padding.get_left() + padding.get_right();
_entry.set_size_request(max(fitWidth, min_width), -1);
queue_resize();
_entry.set_text(val);
if(!_entry.is_visible()) {
_entry.show();
_entry.grab_focus();
// We grab the input for ourselves and not the entry to still have
// the pointer events go through the underlay.
add_modal_grab();
}
}
void stop_editing() {
if(_entry.is_visible()) {
remove_modal_grab();
_entry.hide();
_gl_widget.grab_focus();
}
}
GtkGLWidget &get_gl_widget() {
return _gl_widget;
}
protected:
bool on_key_press_event(GdkEventKey *gdk_event) override {
if(is_editing()) {
if(gdk_event->keyval == GDK_KEY_Escape) {
stop_editing();
} else {
_entry.event((GdkEvent *)gdk_event);
}
return true;
}
return Gtk::Fixed::on_key_press_event(gdk_event);
}
bool on_key_release_event(GdkEventKey *gdk_event) override {
if(is_editing()) {
_entry.event((GdkEvent *)gdk_event);
return true;
}
return Gtk::Fixed::on_key_release_event(gdk_event);
}
void on_size_allocate(Gtk::Allocation& allocation) override {
Gtk::Fixed::on_size_allocate(allocation);
_gl_widget.size_allocate(allocation);
int width, height, min_height, natural_height;
_entry.get_size_request(width, height);
_entry.get_preferred_height(min_height, natural_height);
Gtk::Allocation entry_rect = _entry.get_allocation();
entry_rect.set_width(width);
entry_rect.set_height(natural_height);
_entry.size_allocate(entry_rect);
}
void on_activate() {
if(_receiver->onEditingDone) {
_receiver->onEditingDone(_entry.get_text());
}
}
};
class GtkWindow : public Gtk::Window {
Platform::Window *_receiver;
Gtk::VBox _vbox;
Gtk::MenuBar *_menu_bar;
Gtk::HBox _hbox;
GtkEditorOverlay _editor_overlay;
Gtk::VScrollbar _scrollbar;
bool _is_fullscreen;
public:
GtkWindow(Platform::Window *receiver) :
_receiver(receiver), _menu_bar(NULL), _editor_overlay(receiver) {
_hbox.pack_start(_editor_overlay, /*expand=*/true, /*fill=*/true);
_hbox.pack_end(_scrollbar, /*expand=*/false, /*fill=*/false);
_vbox.pack_end(_hbox, /*expand=*/true, /*fill=*/true);
add(_vbox);
_vbox.show();
_hbox.show();
_editor_overlay.show();
get_gl_widget().show();
_scrollbar.get_adjustment()->signal_value_changed().
connect(sigc::mem_fun(this, &GtkWindow::on_scrollbar_value_changed));
}
bool is_full_screen() const {
return _is_fullscreen;
}
Gtk::MenuBar *get_menu_bar() const {
return _menu_bar;
}
void set_menu_bar(Gtk::MenuBar *menu_bar) {
if(_menu_bar) {
_vbox.remove(*_menu_bar);
}
_menu_bar = menu_bar;
if(_menu_bar) {
_menu_bar->show_all();
_vbox.pack_start(*_menu_bar, /*expand=*/false, /*fill=*/false);
}
}
GtkEditorOverlay &get_editor_overlay() {
return _editor_overlay;
}
GtkGLWidget &get_gl_widget() {
return _editor_overlay.get_gl_widget();
}
Gtk::VScrollbar &get_scrollbar() {
return _scrollbar;
}
protected:
bool on_delete_event(GdkEventAny* gdk_event) {
if(_receiver->onClose) {
_receiver->onClose();
return true;
}
return false;
}
bool on_window_state_event(GdkEventWindowState *gdk_event) override {
_is_fullscreen = gdk_event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
if(_receiver->onFullScreen) {
_receiver->onFullScreen(_is_fullscreen);
}
return Gtk::Window::on_window_state_event(gdk_event);
}
void on_scrollbar_value_changed() {
if(_receiver->onScrollbarAdjusted) {
_receiver->onScrollbarAdjusted(_scrollbar.get_adjustment()->get_value());
}
}
};
//-----------------------------------------------------------------------------
// Windows
//-----------------------------------------------------------------------------
class WindowImplGtk : public Window {
public:
GtkWindow gtkWindow;
MenuBarRef menuBar;
WindowImplGtk(Window::Kind kind) : gtkWindow(this) {
switch(kind) {
case Kind::TOPLEVEL:
break;
case Kind::TOOL:
gtkWindow.set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY);
gtkWindow.set_skip_taskbar_hint(true);
gtkWindow.set_skip_pager_hint(true);
break;
}
auto icon = LoadPng("freedesktop/solvespace-48x48.png");
auto gdkIcon =
Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB,
icon->format == Pixmap::Format::RGBA, 8,
icon->width, icon->height, icon->stride);
gtkWindow.set_icon(gdkIcon->copy());
}
double GetPixelDensity() override {
return gtkWindow.get_screen()->get_resolution();
}
int GetDevicePixelRatio() override {
return gtkWindow.get_scale_factor();
}
bool IsVisible() override {
return gtkWindow.is_visible();
}
void SetVisible(bool visible) override {
if(visible) {
gtkWindow.show();
} else {
gtkWindow.hide();
}
}
void Focus() override {
gtkWindow.present();
}
bool IsFullScreen() override {
return gtkWindow.is_full_screen();
}
void SetFullScreen(bool fullScreen) override {
if(fullScreen) {
gtkWindow.fullscreen();
} else {
gtkWindow.unfullscreen();
}
}
void SetTitle(const std::string &title) override {
gtkWindow.set_title(title + " — SolveSpace");
}
void SetMenuBar(MenuBarRef newMenuBar) override {
if(newMenuBar) {
Gtk::MenuBar *gtkMenuBar = &((MenuBarImplGtk*)&*newMenuBar)->gtkMenuBar;
gtkWindow.set_menu_bar(gtkMenuBar);
} else {
gtkWindow.set_menu_bar(NULL);
}
menuBar = newMenuBar;
}
void GetContentSize(double *width, double *height) override {
*width = gtkWindow.get_gl_widget().get_allocated_width();
*height = gtkWindow.get_gl_widget().get_allocated_height();
}
void SetMinContentSize(double width, double height) override {
gtkWindow.get_gl_widget().set_size_request(width, height);
}
void FreezePosition(SettingsRef settings, const std::string &key) override {
if(!gtkWindow.is_visible()) return;
int left, top, width, height;
gtkWindow.get_position(left, top);
gtkWindow.get_size(width, height);
bool isMaximized = gtkWindow.is_maximized();
settings->FreezeInt(key + "_Left", left);
settings->FreezeInt(key + "_Top", top);
settings->FreezeInt(key + "_Width", width);
settings->FreezeInt(key + "_Height", height);
settings->FreezeBool(key + "_Maximized", isMaximized);
}
void ThawPosition(SettingsRef settings, const std::string &key) override {
int left, top, width, height;
gtkWindow.get_position(left, top);
gtkWindow.get_size(width, height);
left = settings->ThawInt(key + "_Left", left);
top = settings->ThawInt(key + "_Top", top);
width = settings->ThawInt(key + "_Width", width);
height = settings->ThawInt(key + "_Height", height);
gtkWindow.move(left, top);
gtkWindow.resize(width, height);
if(settings->ThawBool(key + "_Maximized", false)) {
gtkWindow.maximize();
}
}
void SetCursor(Cursor cursor) override {
Gdk::CursorType gdkCursorType;
switch(cursor) {
case Cursor::POINTER: gdkCursorType = Gdk::ARROW; break;
case Cursor::HAND: gdkCursorType = Gdk::HAND1; break;
}
auto gdkWindow = gtkWindow.get_gl_widget().get_window();
if(gdkWindow) {
gdkWindow->set_cursor(Gdk::Cursor::create(gdkCursorType));
}
}
void SetTooltip(const std::string &text) override {
if(text.empty()) {
gtkWindow.get_gl_widget().set_has_tooltip(false);
} else {
gtkWindow.get_gl_widget().set_tooltip_text(text);
}
}
bool IsEditorVisible() override {
return gtkWindow.get_editor_overlay().is_editing();
}
void ShowEditor(double x, double y, double fontHeight, double minWidth,
bool isMonospace, const std::string &text) override {
gtkWindow.get_editor_overlay().start_editing(
x, y, fontHeight, minWidth, isMonospace, text);
}
void HideEditor() override {
gtkWindow.get_editor_overlay().stop_editing();
}
void SetScrollbarVisible(bool visible) override {
if(visible) {
gtkWindow.get_scrollbar().show();
} else {
gtkWindow.get_scrollbar().hide();
}
}
void ConfigureScrollbar(double min, double max, double pageSize) override {
auto adjustment = gtkWindow.get_scrollbar().get_adjustment();
adjustment->configure(adjustment->get_value(), min, max, 1, 4, pageSize);
}
double GetScrollbarPosition() override {
return gtkWindow.get_scrollbar().get_adjustment()->get_value();
}
void SetScrollbarPosition(double pos) override {
return gtkWindow.get_scrollbar().get_adjustment()->set_value(pos);
}
void Invalidate() override {
gtkWindow.get_gl_widget().queue_render();
}
void Redraw() override {
Invalidate();
Gtk::Main::iteration(/*blocking=*/false);
}
void *NativePtr() override {
return &gtkWindow;
}
};
WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
auto window = std::make_shared<WindowImplGtk>(kind);
if(parentWindow) {
window->gtkWindow.set_transient_for(
std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow);
}
return window;
}
//-----------------------------------------------------------------------------
// Application-wide APIs
//-----------------------------------------------------------------------------
void Exit() {
Gtk::Main::quit();
}
}
}