2018-07-11 13:35:31 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// The GTK-based implementation of platform-dependent GUI functionality.
|
|
|
|
//
|
|
|
|
// Copyright 2018 whitequark
|
|
|
|
//-----------------------------------------------------------------------------
|
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-18 10:20:25 +08:00
|
|
|
#include <glibmm/convert.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-18 10:20:25 +08:00
|
|
|
#include <gtkmm/cssprovider.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
#include <gtkmm/entry.h>
|
2018-07-18 02:51:00 +08:00
|
|
|
#include <gtkmm/filechooserdialog.h>
|
2019-05-09 06:53:25 +08:00
|
|
|
#if defined(HAVE_GTK_FILECHOOSERNATIVE)
|
|
|
|
# include <gtkmm/filechoosernative.h>
|
|
|
|
#endif
|
2018-07-13 03:29:44 +08:00
|
|
|
#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-18 02:51:00 +08:00
|
|
|
#include <gtkmm/messagedialog.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
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
#if defined(HAVE_SPACEWARE)
|
|
|
|
# include <spnav.h>
|
|
|
|
# include <gdk/gdkx.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include "solvespace.h"
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
namespace SolveSpace {
|
|
|
|
namespace Platform {
|
|
|
|
|
2018-07-17 23:00:46 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Utility functions
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
static std::string PrepareMnemonics(std::string label) {
|
|
|
|
std::replace(label.begin(), label.end(), '&', '_');
|
|
|
|
return label;
|
|
|
|
}
|
|
|
|
|
|
|
|
static std::string PrepareTitle(std::string title) {
|
|
|
|
return title + " — SolveSpace";
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class SettingsImplGtk final : public Settings {
|
2018-07-16 18:37:41 +08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-07-18 00:05:15 +08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class TimerImplGtk final : public Timer {
|
2018-07-11 13:35:31 +08:00
|
|
|
public:
|
|
|
|
sigc::connection _connection;
|
|
|
|
|
Eliminate imperative redraws.
This commit removes Platform::Window::Redraw function, and rewrites
its uses to run on timer events. Most UI toolkits have obscure issues
with recursive event handling loops, and Emscripten is purely event-
driven and cannot handle imperative redraws at all.
As a part of this change, the Platform::Timer::WindUp function
is split into three to make the interpretation of its argument
less magical. The new functions are RunAfter (a regular timeout,
setTimeout in browser terms), RunAfterNextFrame (an animation
request, requestAnimationFrame in browser terms), and
RunAfterProcessingEvents (a request to run something after all
events for the current frame are processed, used for coalescing
expensive operations in face of input event queues).
This commit changes two uses of Redraw(): the AnimateOnto() and
ScreenStepDimGo() functions. The latter was actually broken in that
on small sketches, it would run very quickly and not animate
the dimension change at all; this has been fixed.
While we're at it, get rid of unused Platform::Window::NativePtr
function as well.
2018-07-19 07:11:49 +08:00
|
|
|
void RunAfter(unsigned milliseconds) override {
|
2018-07-11 13:35:31 +08:00
|
|
|
if(!_connection.empty()) {
|
|
|
|
_connection.disconnect();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto handler = [this]() {
|
|
|
|
if(this->onTimeout) {
|
|
|
|
this->onTimeout();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
_connection = Glib::signal_timeout().connect(handler, milliseconds);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
TimerRef CreateTimer() {
|
2018-07-19 07:49:51 +08:00
|
|
|
return std::make_shared<TimerImplGtk>();
|
2018-07-11 13:35:31 +08:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MenuItemImplGtk final : public MenuItem {
|
2018-07-11 18:48:38 +08:00
|
|
|
public:
|
|
|
|
GtkMenuItem gtkMenuItem;
|
|
|
|
|
|
|
|
MenuItemImplGtk() : gtkMenuItem(this) {}
|
|
|
|
|
|
|
|
void SetAccelerator(KeyboardEvent accel) override {
|
2018-07-18 10:20:25 +08:00
|
|
|
guint accelKey = 0;
|
2018-07-11 18:48:38 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MenuImplGtk final : public Menu {
|
2018-07-11 18:48:38 +08:00
|
|
|
public:
|
|
|
|
Gtk::Menu gtkMenu;
|
|
|
|
std::vector<std::shared_ptr<MenuItemImplGtk>> menuItems;
|
|
|
|
std::vector<std::shared_ptr<MenuImplGtk>> subMenus;
|
|
|
|
|
|
|
|
MenuItemRef AddItem(const std::string &label,
|
2019-04-13 19:00:35 +08:00
|
|
|
std::function<void()> onTrigger = NULL,
|
|
|
|
bool mnemonics = true) override {
|
2018-07-11 18:48:38 +08:00
|
|
|
auto menuItem = std::make_shared<MenuItemImplGtk>();
|
|
|
|
menuItems.push_back(menuItem);
|
|
|
|
|
2019-04-13 19:00:35 +08:00
|
|
|
menuItem->gtkMenuItem.set_label(mnemonics ? PrepareMnemonics(label) : label);
|
|
|
|
menuItem->gtkMenuItem.set_use_underline(mnemonics);
|
2018-07-11 18:48:38 +08:00
|
|
|
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);
|
|
|
|
|
2018-07-17 23:00:46 +08:00
|
|
|
menuItem->gtkMenuItem.set_label(PrepareMnemonics(label));
|
2018-07-11 18:48:38 +08:00
|
|
|
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>();
|
|
|
|
}
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MenuBarImplGtk final : public MenuBar {
|
2018-07-11 18:48:38 +08:00
|
|
|
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);
|
2018-07-17 23:00:46 +08:00
|
|
|
gtkMenuItem->set_label(PrepareMnemonics(label));
|
2018-07-11 18:48:38 +08:00
|
|
|
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) {
|
2018-08-01 04:49:56 +08:00
|
|
|
return _gl_widget.event((GdkEvent *)gdk_event);
|
2018-07-13 03:29:44 +08:00
|
|
|
} 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;
|
2018-07-18 10:20:25 +08:00
|
|
|
Gtk::MenuBar *_menu_bar = NULL;
|
2018-07-13 03:29:44 +08:00
|
|
|
Gtk::HBox _hbox;
|
|
|
|
GtkEditorOverlay _editor_overlay;
|
|
|
|
Gtk::VScrollbar _scrollbar;
|
2018-07-18 10:20:25 +08:00
|
|
|
bool _is_fullscreen = false;
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
public:
|
2018-07-18 10:20:25 +08:00
|
|
|
GtkWindow(Platform::Window *receiver) : _receiver(receiver), _editor_overlay(receiver) {
|
2018-07-13 03:29:44 +08:00
|
|
|
_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:
|
2018-08-01 05:03:52 +08:00
|
|
|
bool on_delete_event(GdkEventAny* gdk_event) override {
|
2018-07-13 03:29:44 +08:00
|
|
|
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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class WindowImplGtk final : public Window {
|
2018-07-13 03:29:44 +08:00
|
|
|
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 {
|
2018-07-17 23:00:46 +08:00
|
|
|
gtkWindow.set_title(PrepareTitle(title));
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2018-08-01 05:03:52 +08:00
|
|
|
gtkWindow.get_gl_widget().set_size_request((int)width, (int)height);
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
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;
|
2018-07-18 10:20:25 +08:00
|
|
|
default: ssassert(false, "Unexpected cursor");
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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(
|
2018-08-01 05:03:52 +08:00
|
|
|
(int)x, (int)y, (int)fontHeight, (int)minWidth, isMonospace, text);
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 3DConnexion support
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
void Open3DConnexion() {}
|
|
|
|
void Close3DConnexion() {}
|
|
|
|
|
|
|
|
#if defined(HAVE_SPACEWARE) && defined(GDK_WINDOWING_X11)
|
|
|
|
static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gdkXEvent, GdkEvent *gdkEvent, gpointer data) {
|
|
|
|
XEvent *xEvent = (XEvent *)gdkXEvent;
|
|
|
|
WindowImplGtk *window = (WindowImplGtk *)data;
|
|
|
|
|
|
|
|
spnav_event spnavEvent;
|
|
|
|
if(!spnav_x11_event(xEvent, &spnavEvent)) {
|
|
|
|
return GDK_FILTER_CONTINUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(spnavEvent.type) {
|
|
|
|
case SPNAV_EVENT_MOTION: {
|
|
|
|
SixDofEvent event = {};
|
|
|
|
event.type = SixDofEvent::Type::MOTION;
|
|
|
|
event.translationX = (double)spnavEvent.motion.x;
|
|
|
|
event.translationY = (double)spnavEvent.motion.y;
|
|
|
|
event.translationZ = (double)spnavEvent.motion.z * -1.0;
|
|
|
|
event.rotationX = (double)spnavEvent.motion.rx * 0.001;
|
|
|
|
event.rotationY = (double)spnavEvent.motion.ry * 0.001;
|
|
|
|
event.rotationZ = (double)spnavEvent.motion.rz * -0.001;
|
|
|
|
event.shiftDown = xEvent->xmotion.state & ShiftMask;
|
|
|
|
event.controlDown = xEvent->xmotion.state & ControlMask;
|
|
|
|
if(window->onSixDofEvent) {
|
|
|
|
window->onSixDofEvent(event);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case SPNAV_EVENT_BUTTON:
|
|
|
|
SixDofEvent event = {};
|
|
|
|
if(spnavEvent.button.press) {
|
|
|
|
event.type = SixDofEvent::Type::PRESS;
|
|
|
|
} else {
|
|
|
|
event.type = SixDofEvent::Type::RELEASE;
|
|
|
|
}
|
|
|
|
switch(spnavEvent.button.bnum) {
|
|
|
|
case 0: event.button = SixDofEvent::Button::FIT; break;
|
|
|
|
default: return GDK_FILTER_REMOVE;
|
|
|
|
}
|
|
|
|
event.shiftDown = xEvent->xmotion.state & ShiftMask;
|
|
|
|
event.controlDown = xEvent->xmotion.state & ControlMask;
|
|
|
|
if(window->onSixDofEvent) {
|
|
|
|
window->onSixDofEvent(event);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return GDK_FILTER_REMOVE;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Request3DConnexionEventsForWindow(WindowRef window) {
|
|
|
|
std::shared_ptr<WindowImplGtk> windowImpl =
|
|
|
|
std::static_pointer_cast<WindowImplGtk>(window);
|
|
|
|
|
|
|
|
Glib::RefPtr<Gdk::Window> gdkWindow = windowImpl->gtkWindow.get_window();
|
|
|
|
if(GDK_IS_X11_DISPLAY(gdkWindow->get_display()->gobj())) {
|
|
|
|
gdkWindow->add_filter(GdkSpnavFilter, windowImpl.get());
|
|
|
|
spnav_x11_open(gdk_x11_get_default_xdisplay(),
|
|
|
|
gdk_x11_window_get_xid(gdkWindow->gobj()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
void Request3DConnexionEventsForWindow(WindowRef window) {}
|
|
|
|
#endif
|
|
|
|
|
2018-07-17 23:00:46 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Message dialogs
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
class MessageDialogImplGtk;
|
|
|
|
|
|
|
|
static std::vector<std::shared_ptr<MessageDialogImplGtk>> shownMessageDialogs;
|
|
|
|
|
|
|
|
class MessageDialogImplGtk final : public MessageDialog,
|
|
|
|
public std::enable_shared_from_this<MessageDialogImplGtk> {
|
2018-07-17 23:00:46 +08:00
|
|
|
public:
|
|
|
|
Gtk::Image gtkImage;
|
|
|
|
Gtk::MessageDialog gtkDialog;
|
|
|
|
|
|
|
|
MessageDialogImplGtk(Gtk::Window &parent)
|
|
|
|
: gtkDialog(parent, "", /*use_markup=*/false, Gtk::MESSAGE_INFO,
|
|
|
|
Gtk::BUTTONS_NONE, /*modal=*/true)
|
|
|
|
{
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
SetTitle("Message");
|
2018-07-17 23:00:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void SetType(Type type) override {
|
|
|
|
switch(type) {
|
|
|
|
case Type::INFORMATION:
|
|
|
|
gtkImage.set_from_icon_name("dialog-information", Gtk::ICON_SIZE_DIALOG);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::QUESTION:
|
|
|
|
gtkImage.set_from_icon_name("dialog-question", Gtk::ICON_SIZE_DIALOG);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::WARNING:
|
|
|
|
gtkImage.set_from_icon_name("dialog-warning", Gtk::ICON_SIZE_DIALOG);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::ERROR:
|
|
|
|
gtkImage.set_from_icon_name("dialog-error", Gtk::ICON_SIZE_DIALOG);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
gtkDialog.set_image(gtkImage);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTitle(std::string title) override {
|
|
|
|
gtkDialog.set_title(PrepareTitle(title));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetMessage(std::string message) override {
|
|
|
|
gtkDialog.set_message(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetDescription(std::string description) override {
|
|
|
|
gtkDialog.set_secondary_text(description);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddButton(std::string name, Response response, bool isDefault) override {
|
2018-07-18 10:20:25 +08:00
|
|
|
int responseId = 0;
|
2018-07-17 23:00:46 +08:00
|
|
|
switch(response) {
|
2018-07-18 10:20:25 +08:00
|
|
|
case Response::NONE: ssassert(false, "Unexpected response");
|
2018-07-17 23:00:46 +08:00
|
|
|
case Response::OK: responseId = Gtk::RESPONSE_OK; break;
|
|
|
|
case Response::YES: responseId = Gtk::RESPONSE_YES; break;
|
|
|
|
case Response::NO: responseId = Gtk::RESPONSE_NO; break;
|
|
|
|
case Response::CANCEL: responseId = Gtk::RESPONSE_CANCEL; break;
|
|
|
|
}
|
|
|
|
gtkDialog.add_button(PrepareMnemonics(name), responseId);
|
|
|
|
if(isDefault) {
|
|
|
|
gtkDialog.set_default_response(responseId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
Response ProcessResponse(int gtkResponse) {
|
|
|
|
Response response;
|
|
|
|
switch(gtkResponse) {
|
|
|
|
case Gtk::RESPONSE_OK: response = Response::OK; break;
|
|
|
|
case Gtk::RESPONSE_YES: response = Response::YES; break;
|
|
|
|
case Gtk::RESPONSE_NO: response = Response::NO; break;
|
|
|
|
case Gtk::RESPONSE_CANCEL: response = Response::CANCEL; break;
|
2018-07-17 23:00:46 +08:00
|
|
|
|
|
|
|
case Gtk::RESPONSE_NONE:
|
|
|
|
case Gtk::RESPONSE_CLOSE:
|
|
|
|
case Gtk::RESPONSE_DELETE_EVENT:
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
response = Response::NONE;
|
2018-07-17 23:00:46 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
default: ssassert(false, "Unexpected response");
|
|
|
|
}
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
|
|
|
|
if(onResponse) {
|
|
|
|
onResponse(response);
|
|
|
|
}
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShowModal() override {
|
2018-07-22 17:00:13 +08:00
|
|
|
gtkDialog.signal_hide().connect([this] {
|
|
|
|
auto it = std::remove(shownMessageDialogs.begin(), shownMessageDialogs.end(),
|
|
|
|
shared_from_this());
|
|
|
|
shownMessageDialogs.erase(it);
|
|
|
|
});
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
shownMessageDialogs.push_back(shared_from_this());
|
2018-07-22 17:00:13 +08:00
|
|
|
|
|
|
|
gtkDialog.signal_response().connect([this](int gtkResponse) {
|
|
|
|
ProcessResponse(gtkResponse);
|
2018-08-03 21:34:12 +08:00
|
|
|
gtkDialog.hide();
|
2018-07-22 17:00:13 +08:00
|
|
|
});
|
Eliminate blocking in Error() and Message() calls.
This serves two purposes.
First, we want to (some day) convert these messages into a less
obtrustive form, something like toaster notifications, such that they
don't interrupt workflow as harshly. That would, of course, be
nonblocking.
Second, some platforms, like Emscripten, do not support nested event
loops, and it's not possible to display a modal dialog on them
synchronously.
When making this commit, I've reviewed all Error() and Message()
calls to ensure that only some of the following is true for all
of them:
* The call is followed a break or return statement that exits
an UI entry point (e.g. an MenuX function);
* The call is followed by cleanup (in fact, in this case the new
behavior is better, since even with a synchronous modal dialog
we have to be reentrant);
* The message is an informational message only and nothing
unexpected will happen if the operation proceeds in background.
In general, all Error() calls already satisfied the above conditions,
although in some cases I changed control flow aroudn them to more
clearly show that. The Message() calls that didn't satisfy these
conditions were reworked into an asynchronous form.
There are three explicit RunModal() calls left that need to be
reworked into an async form.
2018-07-20 05:54:05 +08:00
|
|
|
gtkDialog.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
Response RunModal() override {
|
|
|
|
return ProcessResponse(gtkDialog.run());
|
2018-07-17 23:00:46 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
|
|
|
|
return std::make_shared<MessageDialogImplGtk>(
|
|
|
|
std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow);
|
|
|
|
}
|
|
|
|
|
2018-07-18 02:51:00 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// File dialogs
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2019-05-09 06:53:25 +08:00
|
|
|
class FileDialogImplGtk : public FileDialog {
|
2018-07-18 02:51:00 +08:00
|
|
|
public:
|
2019-05-09 06:53:25 +08:00
|
|
|
Gtk::FileChooser *gtkChooser;
|
2018-07-18 02:51:00 +08:00
|
|
|
std::vector<std::string> extensions;
|
|
|
|
|
2019-05-09 06:53:25 +08:00
|
|
|
void InitFileChooser(Gtk::FileChooser &chooser) {
|
|
|
|
gtkChooser = &chooser;
|
|
|
|
gtkChooser->property_filter().signal_changed().
|
2018-07-18 02:51:00 +08:00
|
|
|
connect(sigc::mem_fun(this, &FileDialogImplGtk::FilterChanged));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetCurrentName(std::string name) override {
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->set_current_name(name);
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Platform::Path GetFilename() override {
|
2019-05-09 06:53:25 +08:00
|
|
|
return Path::From(gtkChooser->get_filename());
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void SetFilename(Platform::Path path) override {
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->set_filename(path.raw);
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void AddFilter(std::string name, std::vector<std::string> extensions) override {
|
|
|
|
Glib::RefPtr<Gtk::FileFilter> gtkFilter = Gtk::FileFilter::create();
|
|
|
|
Glib::ustring desc;
|
|
|
|
for(auto extension : extensions) {
|
|
|
|
Glib::ustring pattern = "*";
|
|
|
|
if(!extension.empty()) {
|
|
|
|
pattern = "*." + extension;
|
|
|
|
gtkFilter->add_pattern(pattern);
|
|
|
|
gtkFilter->add_pattern(Glib::ustring(pattern).uppercase());
|
|
|
|
}
|
|
|
|
if(!desc.empty()) {
|
|
|
|
desc += ", ";
|
|
|
|
}
|
|
|
|
desc += pattern;
|
|
|
|
}
|
|
|
|
gtkFilter->set_name(name + " (" + desc + ")");
|
|
|
|
|
|
|
|
this->extensions.push_back(extensions.front());
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->add_filter(gtkFilter);
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string GetExtension() {
|
2019-05-09 06:53:25 +08:00
|
|
|
auto filters = gtkChooser->list_filters();
|
2018-07-18 02:51:00 +08:00
|
|
|
size_t filterIndex =
|
2019-05-09 06:53:25 +08:00
|
|
|
std::find(filters.begin(), filters.end(), gtkChooser->get_filter()) -
|
2018-07-18 02:51:00 +08:00
|
|
|
filters.begin();
|
|
|
|
if(filterIndex < extensions.size()) {
|
|
|
|
return extensions[filterIndex];
|
|
|
|
} else {
|
|
|
|
return extensions.front();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetExtension(std::string extension) {
|
2019-05-09 06:53:25 +08:00
|
|
|
auto filters = gtkChooser->list_filters();
|
2018-07-18 02:51:00 +08:00
|
|
|
size_t extensionIndex =
|
|
|
|
std::find(extensions.begin(), extensions.end(), extension) -
|
|
|
|
extensions.begin();
|
|
|
|
if(extensionIndex < filters.size()) {
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->set_filter(filters[extensionIndex]);
|
2018-07-18 02:51:00 +08:00
|
|
|
} else {
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->set_filter(filters.front());
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void FilterChanged() {
|
|
|
|
std::string extension = GetExtension();
|
|
|
|
if(extension == "") return;
|
|
|
|
|
|
|
|
Platform::Path path = GetFilename();
|
|
|
|
SetCurrentName(path.WithExtension(extension).FileName());
|
|
|
|
}
|
|
|
|
|
|
|
|
void FreezeChoices(SettingsRef settings, const std::string &key) override {
|
|
|
|
settings->FreezeString("Dialog_" + key + "_Folder",
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->get_current_folder());
|
2018-07-18 02:51:00 +08:00
|
|
|
settings->FreezeString("Dialog_" + key + "_Filter", GetExtension());
|
|
|
|
}
|
|
|
|
|
|
|
|
void ThawChoices(SettingsRef settings, const std::string &key) override {
|
2019-05-09 06:53:25 +08:00
|
|
|
gtkChooser->set_current_folder(settings->ThawString("Dialog_" + key + "_Folder"));
|
2018-07-18 02:51:00 +08:00
|
|
|
SetExtension(settings->ThawString("Dialog_" + key + "_Filter"));
|
|
|
|
}
|
|
|
|
|
2019-05-09 06:53:25 +08:00
|
|
|
void CheckForUntitledFile() {
|
|
|
|
if(gtkChooser->get_action() == Gtk::FILE_CHOOSER_ACTION_SAVE &&
|
|
|
|
Path::From(gtkChooser->get_current_name()).FileStem().empty()) {
|
|
|
|
gtkChooser->set_current_name(std::string(_("untitled")) + "." + GetExtension());
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
2019-05-09 06:53:25 +08:00
|
|
|
}
|
|
|
|
};
|
2018-07-18 02:51:00 +08:00
|
|
|
|
2019-05-09 06:53:25 +08:00
|
|
|
class FileDialogGtkImplGtk final : public FileDialogImplGtk {
|
|
|
|
public:
|
|
|
|
Gtk::FileChooserDialog gtkDialog;
|
|
|
|
|
|
|
|
FileDialogGtkImplGtk(Gtk::Window >kParent, bool isSave)
|
|
|
|
: gtkDialog(gtkParent,
|
|
|
|
isSave ? C_("title", "Save File")
|
|
|
|
: C_("title", "Open File"),
|
|
|
|
isSave ? Gtk::FILE_CHOOSER_ACTION_SAVE
|
|
|
|
: Gtk::FILE_CHOOSER_ACTION_OPEN) {
|
|
|
|
gtkDialog.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL);
|
|
|
|
gtkDialog.add_button(isSave ? C_("button", "_Save")
|
|
|
|
: C_("button", "_Open"), Gtk::RESPONSE_OK);
|
|
|
|
gtkDialog.set_default_response(Gtk::RESPONSE_OK);
|
|
|
|
InitFileChooser(gtkDialog);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTitle(std::string title) override {
|
|
|
|
gtkDialog.set_title(PrepareTitle(title));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool RunModal() override {
|
|
|
|
CheckForUntitledFile();
|
2018-07-18 02:51:00 +08:00
|
|
|
if(gtkDialog.run() == Gtk::RESPONSE_OK) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-05-09 06:53:25 +08:00
|
|
|
#if defined(HAVE_GTK_FILECHOOSERNATIVE)
|
|
|
|
|
|
|
|
class FileDialogNativeImplGtk final : public FileDialogImplGtk {
|
|
|
|
public:
|
|
|
|
Glib::RefPtr<Gtk::FileChooserNative> gtkNative;
|
|
|
|
|
|
|
|
FileDialogNativeImplGtk(Gtk::Window >kParent, bool isSave) {
|
|
|
|
gtkNative = Gtk::FileChooserNative::create(
|
|
|
|
isSave ? C_("title", "Save File")
|
|
|
|
: C_("title", "Open File"),
|
|
|
|
gtkParent,
|
|
|
|
isSave ? Gtk::FILE_CHOOSER_ACTION_SAVE
|
|
|
|
: Gtk::FILE_CHOOSER_ACTION_OPEN,
|
|
|
|
isSave ? C_("button", "_Save")
|
|
|
|
: C_("button", "_Open"),
|
|
|
|
C_("button", "_Cancel"));
|
|
|
|
// Seriously, GTK?!
|
|
|
|
InitFileChooser(*gtkNative.operator->());
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTitle(std::string title) override {
|
|
|
|
gtkNative->set_title(PrepareTitle(title));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool RunModal() override {
|
|
|
|
CheckForUntitledFile();
|
|
|
|
if(gtkNative->run() == Gtk::RESPONSE_ACCEPT) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if defined(HAVE_GTK_FILECHOOSERNATIVE)
|
|
|
|
# define FILE_DIALOG_IMPL FileDialogNativeImplGtk
|
|
|
|
#else
|
|
|
|
# define FILE_DIALOG_IMPL FileDialogGtkImplGtk
|
|
|
|
#endif
|
|
|
|
|
2018-07-18 02:51:00 +08:00
|
|
|
FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) {
|
|
|
|
Gtk::Window >kParent = std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow;
|
2019-05-09 06:53:25 +08:00
|
|
|
return std::make_shared<FILE_DIALOG_IMPL>(gtkParent, /*isSave=*/false);
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
|
|
|
|
Gtk::Window >kParent = std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow;
|
2019-05-09 06:53:25 +08:00
|
|
|
return std::make_shared<FILE_DIALOG_IMPL>(gtkParent, /*isSave=*/true);
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Application-wide APIs
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-18 10:20:25 +08:00
|
|
|
std::vector<Platform::Path> GetFontFiles() {
|
|
|
|
std::vector<Platform::Path> fonts;
|
|
|
|
|
|
|
|
// fontconfig is already initialized by GTK
|
|
|
|
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}");
|
|
|
|
fonts.push_back(Platform::Path::From((const char *)filenameFC));
|
|
|
|
FcStrFree(filenameFC);
|
|
|
|
}
|
|
|
|
|
|
|
|
FcFontSetDestroy(fs);
|
|
|
|
FcObjectSetDestroy(os);
|
|
|
|
FcPatternDestroy(pat);
|
|
|
|
|
|
|
|
return fonts;
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenInBrowser(const std::string &url) {
|
|
|
|
gtk_show_uri(Gdk::Screen::get_default()->gobj(), url.c_str(), GDK_CURRENT_TIME, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
Gtk::Main *gtkMain;
|
|
|
|
|
|
|
|
void InitGui(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 so that printf() and friends behave in a consistent way.
|
|
|
|
setlocale(LC_ALL, "");
|
|
|
|
if(!Glib::get_charset()) {
|
|
|
|
dbp("Sorry, only UTF-8 locales are supported.");
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
setlocale(LC_ALL, "C");
|
|
|
|
|
|
|
|
gtkMain = new Gtk::Main(argc, argv, /*set_locale=*/false);
|
|
|
|
|
|
|
|
// Add our application-specific styles, to override GTK defaults.
|
|
|
|
Glib::RefPtr<Gtk::CssProvider> style_provider = Gtk::CssProvider::create();
|
|
|
|
style_provider->load_from_data(R"(
|
|
|
|
entry {
|
|
|
|
background: white;
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
)");
|
|
|
|
Gtk::StyleContext::add_provider_for_screen(
|
|
|
|
Gdk::Screen::get_default(), style_provider,
|
|
|
|
600 /*Gtk::STYLE_PROVIDER_PRIORITY_APPLICATION*/);
|
|
|
|
|
|
|
|
// Set locale from user preferences.
|
|
|
|
// This apparently only consults the LANGUAGE environment variable.
|
|
|
|
const char* const* langNames = g_get_language_names();
|
|
|
|
while(*langNames) {
|
|
|
|
if(SetLocale(*langNames++)) break;
|
|
|
|
}
|
|
|
|
if(!*langNames) {
|
|
|
|
SetLocale("en_US");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void RunGui() {
|
|
|
|
gtkMain->run();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ExitGui() {
|
|
|
|
gtkMain->quit();
|
|
|
|
delete gtkMain;
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
}
|
|
|
|
}
|