2018-07-11 13:35:31 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// The GTK-based implementation of platform-dependent GUI functionality.
|
|
|
|
//
|
|
|
|
// Copyright 2018 whitequark
|
|
|
|
//-----------------------------------------------------------------------------
|
2018-07-13 03:29:44 +08:00
|
|
|
#include "solvespace.h"
|
2018-07-16 18:37:41 +08:00
|
|
|
#include <errno.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <json-c/json_object.h>
|
|
|
|
#include <json-c/json_util.h>
|
2018-07-11 13:35:31 +08:00
|
|
|
#include <glibmm/main.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
#include <gtkmm/box.h>
|
2018-07-11 18:48:38 +08:00
|
|
|
#include <gtkmm/checkmenuitem.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
#include <gtkmm/entry.h>
|
|
|
|
#include <gtkmm/fixed.h>
|
|
|
|
#include <gtkmm/glarea.h>
|
|
|
|
#include <gtkmm/main.h>
|
2018-07-11 18:48:38 +08:00
|
|
|
#include <gtkmm/menu.h>
|
|
|
|
#include <gtkmm/menubar.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
#include <gtkmm/scrollbar.h>
|
|
|
|
#include <gtkmm/separatormenuitem.h>
|
|
|
|
#include <gtkmm/window.h>
|
2018-07-11 13:35:31 +08:00
|
|
|
|
|
|
|
namespace SolveSpace {
|
|
|
|
namespace Platform {
|
|
|
|
|
2018-07-17 20:32:58 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Fatal errors
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
void FatalError(std::string message) {
|
|
|
|
fprintf(stderr, "%s", message.c_str());
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 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 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;
|
|
|
|
}
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2018-07-11 18:48:38 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 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>();
|
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
void FreezePosition(SettingsRef settings, const std::string &key) override {
|
2018-07-13 03:29:44 +08:00
|
|
|
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();
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
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);
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
void ThawPosition(SettingsRef settings, const std::string &key) override {
|
2018-07-13 03:29:44 +08:00
|
|
|
int left, top, width, height;
|
|
|
|
gtkWindow.get_position(left, top);
|
|
|
|
gtkWindow.get_size(width, height);
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
left = settings->ThawInt(key + "_Left", left);
|
|
|
|
top = settings->ThawInt(key + "_Top", top);
|
|
|
|
width = settings->ThawInt(key + "_Width", width);
|
|
|
|
height = settings->ThawInt(key + "_Height", height);
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
gtkWindow.move(left, top);
|
|
|
|
gtkWindow.resize(width, height);
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
if(settings->ThawBool(key + "_Maximized", false)) {
|
2018-07-13 03:29:44 +08:00
|
|
|
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 >kWindow;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
}
|
|
|
|
}
|