2015-03-19 01:02:11 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
2016-04-22 02:24:49 +08:00
|
|
|
// Our main() function, and GTK2/3-specific stuff to set up our windows and
|
2015-03-19 01:02:11 +08:00
|
|
|
// otherwise handle our interface to the operating system. Everything
|
2016-04-22 02:24:49 +08:00
|
|
|
// outside platform/... should be standard C++ and OpenGL.
|
2015-03-19 01:02:11 +08:00
|
|
|
//
|
|
|
|
// Copyright 2015 <whitequark@whitequark.org>
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include <errno.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <time.h>
|
|
|
|
|
|
|
|
#include <iostream>
|
|
|
|
|
|
|
|
#include <json-c/json_object.h>
|
|
|
|
#include <json-c/json_util.h>
|
|
|
|
|
|
|
|
#include <glibmm/main.h>
|
2015-12-30 22:59:04 +08:00
|
|
|
#include <glibmm/convert.h>
|
2015-03-19 01:02:11 +08:00
|
|
|
#include <giomm/file.h>
|
|
|
|
#include <gdkmm/cursor.h>
|
|
|
|
#include <gtkmm/drawingarea.h>
|
|
|
|
#include <gtkmm/scrollbar.h>
|
|
|
|
#include <gtkmm/entry.h>
|
|
|
|
#include <gtkmm/eventbox.h>
|
|
|
|
#include <gtkmm/fixed.h>
|
|
|
|
#include <gtkmm/adjustment.h>
|
|
|
|
#include <gtkmm/separatormenuitem.h>
|
|
|
|
#include <gtkmm/menuitem.h>
|
|
|
|
#include <gtkmm/checkmenuitem.h>
|
|
|
|
#include <gtkmm/radiomenuitem.h>
|
|
|
|
#include <gtkmm/radiobuttongroup.h>
|
|
|
|
#include <gtkmm/menu.h>
|
|
|
|
#include <gtkmm/menubar.h>
|
|
|
|
#include <gtkmm/scrolledwindow.h>
|
|
|
|
#include <gtkmm/filechooserdialog.h>
|
|
|
|
#include <gtkmm/messagedialog.h>
|
|
|
|
#include <gtkmm/main.h>
|
|
|
|
|
|
|
|
#if HAVE_GTK3
|
|
|
|
#include <gtkmm/hvbox.h>
|
|
|
|
#else
|
|
|
|
#include <gtkmm/box.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <cairomm/xlib_surface.h>
|
|
|
|
#include <pangomm/fontdescription.h>
|
|
|
|
#include <gdk/gdkx.h>
|
|
|
|
#include <fontconfig/fontconfig.h>
|
|
|
|
|
|
|
|
#include <GL/glx.h>
|
|
|
|
|
2015-03-21 03:35:04 +08:00
|
|
|
#include "solvespace.h"
|
2016-04-24 07:00:16 +08:00
|
|
|
#include "config.h"
|
2016-04-22 02:24:49 +08:00
|
|
|
#include "gloffscreen.h"
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
#ifdef HAVE_SPACEWARE
|
|
|
|
# include <spnav.h>
|
|
|
|
# ifndef SI_APP_FIT_BUTTON
|
|
|
|
# define SI_APP_FIT_BUTTON 31
|
|
|
|
# endif
|
|
|
|
#endif
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
namespace SolveSpace {
|
2015-03-19 01:02:11 +08:00
|
|
|
/* Settings */
|
|
|
|
|
|
|
|
/* Why not just use GSettings? Two reasons. It doesn't allow to easily see
|
|
|
|
whether the setting had the default value, and it requires to install
|
|
|
|
a schema globally. */
|
|
|
|
static json_object *settings = NULL;
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
static std::string CnfPrepare() {
|
2015-03-19 01:02:11 +08:00
|
|
|
// Refer to http://standards.freedesktop.org/basedir-spec/latest/
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
std::string dir;
|
|
|
|
if(getenv("XDG_CONFIG_HOME")) {
|
|
|
|
dir = std::string(getenv("XDG_CONFIG_HOME")) + "/solvespace";
|
|
|
|
} else if(getenv("HOME")) {
|
|
|
|
dir = std::string(getenv("HOME")) + "/.config/solvespace";
|
|
|
|
} else {
|
|
|
|
dbp("neither XDG_CONFIG_HOME nor HOME are set");
|
|
|
|
return "";
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
struct stat st;
|
2015-12-26 23:54:26 +08:00
|
|
|
if(stat(dir.c_str(), &st)) {
|
2015-03-19 01:02:11 +08:00
|
|
|
if(errno == ENOENT) {
|
2015-12-26 23:54:26 +08:00
|
|
|
if(mkdir(dir.c_str(), 0777)) {
|
|
|
|
dbp("cannot mkdir %s: %s", dir.c_str(), strerror(errno));
|
|
|
|
return "";
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
} else {
|
2015-12-26 23:54:26 +08:00
|
|
|
dbp("cannot stat %s: %s", dir.c_str(), strerror(errno));
|
|
|
|
return "";
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
} else if(!S_ISDIR(st.st_mode)) {
|
2015-12-26 23:54:26 +08:00
|
|
|
dbp("%s is not a directory", dir.c_str());
|
|
|
|
return "";
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
return dir + "/settings.json";
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
static void CnfLoad() {
|
2015-12-26 23:54:26 +08:00
|
|
|
std::string path = CnfPrepare();
|
|
|
|
if(path.empty())
|
2015-03-19 01:02:11 +08:00
|
|
|
return;
|
|
|
|
|
|
|
|
if(settings)
|
|
|
|
json_object_put(settings); // deallocate
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
settings = json_object_from_file(path.c_str());
|
2015-03-19 01:02:11 +08:00
|
|
|
if(!settings) {
|
|
|
|
if(errno != ENOENT)
|
|
|
|
dbp("cannot load settings: %s", strerror(errno));
|
|
|
|
|
|
|
|
settings = json_object_new_object();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CnfSave() {
|
2015-12-26 23:54:26 +08:00
|
|
|
std::string path = CnfPrepare();
|
|
|
|
if(path.empty())
|
2015-03-19 01:02:11 +08:00
|
|
|
return;
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
/* json-c <0.12 has the first argument non-const here */
|
|
|
|
if(json_object_to_file_ext((char*) path.c_str(), settings, JSON_C_TO_STRING_PRETTY))
|
2015-03-19 01:02:11 +08:00
|
|
|
dbp("cannot save settings: %s", strerror(errno));
|
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
void CnfFreezeInt(uint32_t val, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
struct json_object *jval = json_object_new_int(val);
|
2015-12-26 23:54:26 +08:00
|
|
|
json_object_object_add(settings, key.c_str(), jval);
|
2015-03-19 01:02:11 +08:00
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
uint32_t CnfThawInt(uint32_t val, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
struct json_object *jval;
|
2015-12-26 23:54:26 +08:00
|
|
|
if(json_object_object_get_ex(settings, key.c_str(), &jval))
|
2015-03-19 01:02:11 +08:00
|
|
|
return json_object_get_int(jval);
|
|
|
|
else return val;
|
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
void CnfFreezeFloat(float val, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
struct json_object *jval = json_object_new_double(val);
|
2015-12-26 23:54:26 +08:00
|
|
|
json_object_object_add(settings, key.c_str(), jval);
|
2015-03-19 01:02:11 +08:00
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
float CnfThawFloat(float val, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
struct json_object *jval;
|
2015-12-26 23:54:26 +08:00
|
|
|
if(json_object_object_get_ex(settings, key.c_str(), &jval))
|
2015-03-19 01:02:11 +08:00
|
|
|
return json_object_get_double(jval);
|
|
|
|
else return val;
|
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
void CnfFreezeString(const std::string &val, const std::string &key) {
|
|
|
|
struct json_object *jval = json_object_new_string(val.c_str());
|
|
|
|
json_object_object_add(settings, key.c_str(), jval);
|
2015-03-19 01:02:11 +08:00
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
std::string CnfThawString(const std::string &val, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
struct json_object *jval;
|
2015-12-26 23:54:26 +08:00
|
|
|
if(json_object_object_get_ex(settings, key.c_str(), &jval))
|
|
|
|
return json_object_get_string(jval);
|
|
|
|
return val;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
static void CnfFreezeWindowPos(Gtk::Window *win, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
int x, y, w, h;
|
|
|
|
win->get_position(x, y);
|
|
|
|
win->get_size(w, h);
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
CnfFreezeInt(x, key + "_left");
|
|
|
|
CnfFreezeInt(y, key + "_top");
|
|
|
|
CnfFreezeInt(w, key + "_width");
|
|
|
|
CnfFreezeInt(h, key + "_height");
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
static void CnfThawWindowPos(Gtk::Window *win, const std::string &key) {
|
2015-03-19 01:02:11 +08:00
|
|
|
int x, y, w, h;
|
|
|
|
win->get_position(x, y);
|
|
|
|
win->get_size(w, h);
|
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
x = CnfThawInt(x, key + "_left");
|
|
|
|
y = CnfThawInt(y, key + "_top");
|
|
|
|
w = CnfThawInt(w, key + "_width");
|
|
|
|
h = CnfThawInt(h, key + "_height");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
win->move(x, y);
|
|
|
|
win->resize(w, h);
|
|
|
|
}
|
|
|
|
|
2015-03-29 12:46:57 +08:00
|
|
|
/* Timers */
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
int64_t GetMilliseconds(void) {
|
|
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
|
|
return 1000 * (uint64_t) ts.tv_sec + ts.tv_nsec / 1000000;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool TimerCallback() {
|
|
|
|
SS.GW.TimerCallback();
|
|
|
|
SS.TW.TimerCallback();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTimerFor(int milliseconds) {
|
|
|
|
Glib::signal_timeout().connect(&TimerCallback, milliseconds);
|
|
|
|
}
|
|
|
|
|
2015-03-29 12:46:57 +08:00
|
|
|
static bool AutosaveTimerCallback() {
|
|
|
|
SS.Autosave();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetAutosaveTimerFor(int minutes) {
|
|
|
|
Glib::signal_timeout().connect(&AutosaveTimerCallback, minutes * 60 * 1000);
|
|
|
|
}
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
static bool LaterCallback() {
|
|
|
|
SS.DoLater();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ScheduleLater() {
|
|
|
|
Glib::signal_idle().connect(&LaterCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* GL wrapper */
|
|
|
|
|
2015-12-27 09:03:24 +08:00
|
|
|
#define GL_CHECK() \
|
|
|
|
do { \
|
|
|
|
int err = (int)glGetError(); \
|
|
|
|
if(err) dbp("%s:%d: glGetError() == 0x%X %s", \
|
|
|
|
__FILE__, __LINE__, err, gluErrorString(err)); \
|
|
|
|
} while (0)
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
class GlWidget : public Gtk::DrawingArea {
|
|
|
|
public:
|
2015-03-21 03:35:04 +08:00
|
|
|
GlWidget() : _offscreen(NULL) {
|
2015-03-20 20:47:55 +08:00
|
|
|
_xdisplay = gdk_x11_get_default_xdisplay();
|
|
|
|
|
|
|
|
int glxmajor, glxminor;
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(glXQueryVersion(_xdisplay, &glxmajor, &glxminor),
|
|
|
|
"Expected OpenGL to be available");
|
2015-03-20 20:47:55 +08:00
|
|
|
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(glxmajor > 1 || (glxmajor == 1 && glxminor >= 3),
|
|
|
|
"Expected GLX >= 1.3");
|
2015-03-20 20:47:55 +08:00
|
|
|
|
|
|
|
static int fbconfig_attrs[] = {
|
|
|
|
GLX_RENDER_TYPE, GLX_RGBA_BIT,
|
2015-03-19 01:02:11 +08:00
|
|
|
GLX_RED_SIZE, 8,
|
|
|
|
GLX_GREEN_SIZE, 8,
|
|
|
|
GLX_BLUE_SIZE, 8,
|
|
|
|
GLX_DEPTH_SIZE, 24,
|
|
|
|
None
|
|
|
|
};
|
2015-03-20 20:47:55 +08:00
|
|
|
int fbconfig_num = 0;
|
|
|
|
GLXFBConfig *fbconfigs = glXChooseFBConfig(_xdisplay, DefaultScreen(_xdisplay),
|
|
|
|
fbconfig_attrs, &fbconfig_num);
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(fbconfigs && fbconfig_num > 0,
|
|
|
|
"Expected an available framebuffer configuration");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2015-03-20 20:47:55 +08:00
|
|
|
/* prefer FBConfigs with depth of 32;
|
|
|
|
* Mesa software rasterizer explodes with a BadMatch without this;
|
|
|
|
* without this, Intel on Mesa flickers horribly for some reason.
|
2015-03-21 03:35:04 +08:00
|
|
|
this does not seem to affect other rasterizers (ie NVidia).
|
|
|
|
|
|
|
|
see this Mesa bug:
|
|
|
|
http://lists.freedesktop.org/archives/mesa-dev/2015-January/074693.html */
|
2015-03-20 20:47:55 +08:00
|
|
|
GLXFBConfig fbconfig = fbconfigs[0];
|
|
|
|
for(int i = 0; i < fbconfig_num; i++) {
|
|
|
|
XVisualInfo *visual_info = glXGetVisualFromFBConfig(_xdisplay, fbconfigs[i]);
|
2015-03-21 06:59:48 +08:00
|
|
|
/* some GL visuals, notably on Chromium GL, do not have an associated
|
|
|
|
X visual; this is not an obstacle as we always render offscreen. */
|
|
|
|
if(!visual_info) continue;
|
2015-03-20 20:47:55 +08:00
|
|
|
int depth = visual_info->depth;
|
|
|
|
XFree(visual_info);
|
|
|
|
|
|
|
|
if(depth == 32) {
|
|
|
|
fbconfig = fbconfigs[i];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_glcontext = glXCreateNewContext(_xdisplay,
|
|
|
|
fbconfig, GLX_RGBA_TYPE, 0, True);
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(_glcontext != NULL, "Cannot create an OpenGL context");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2015-03-20 20:47:55 +08:00
|
|
|
XFree(fbconfigs);
|
2015-03-21 06:59:48 +08:00
|
|
|
|
|
|
|
/* create a dummy X window to create a rendering context against.
|
|
|
|
we could use a Pbuffer, but some implementations (Chromium GL)
|
|
|
|
don't support these. we could use an existing window, but
|
|
|
|
some implementations (Chromium GL... do you see a pattern?)
|
|
|
|
do really strange things, i.e. draw a black rectangle on
|
|
|
|
the very front of the desktop if you do this. */
|
|
|
|
_xwindow = XCreateSimpleWindow(_xdisplay,
|
|
|
|
XRootWindow(_xdisplay, gdk_x11_get_default_screen()),
|
|
|
|
/*x*/ 0, /*y*/ 0, /*width*/ 1, /*height*/ 1,
|
|
|
|
/*border_width*/ 0, /*border*/ 0, /*background*/ 0);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~GlWidget() {
|
2015-03-21 06:59:48 +08:00
|
|
|
glXMakeCurrent(_xdisplay, None, NULL);
|
|
|
|
|
|
|
|
XDestroyWindow(_xdisplay, _xwindow);
|
|
|
|
|
2015-03-21 03:35:04 +08:00
|
|
|
delete _offscreen;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2015-03-20 20:47:55 +08:00
|
|
|
glXDestroyContext(_xdisplay, _glcontext);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2015-03-21 03:35:04 +08:00
|
|
|
/* Draw on a GLX framebuffer object, then read pixels out and draw them on
|
2015-03-19 01:02:11 +08:00
|
|
|
the Cairo context. Slower, but you get to overlay nice widgets. */
|
2016-05-19 02:42:33 +08:00
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override {
|
|
|
|
#else
|
|
|
|
bool on_expose_event(GdkEventExpose *) override {
|
|
|
|
const Cairo::RefPtr<Cairo::Context> &cr = get_window()->create_cairo_context();
|
|
|
|
#endif
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(glXMakeCurrent(_xdisplay, _xwindow, _glcontext),
|
|
|
|
"Cannot make OpenGL context current");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2015-03-21 03:35:04 +08:00
|
|
|
if(!_offscreen)
|
|
|
|
_offscreen = new GLOffscreen;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
Gdk::Rectangle allocation = get_allocation();
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(_offscreen->begin(allocation.get_width(), allocation.get_height()),
|
|
|
|
"Cannot allocate offscreen rendering buffer");
|
2015-03-20 20:47:55 +08:00
|
|
|
|
2015-03-21 03:35:04 +08:00
|
|
|
on_gl_draw();
|
|
|
|
glFlush();
|
|
|
|
GL_CHECK();
|
2015-03-20 20:47:55 +08:00
|
|
|
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface> surface = Cairo::ImageSurface::create(
|
2015-03-21 03:35:04 +08:00
|
|
|
_offscreen->end(), Cairo::FORMAT_RGB24,
|
|
|
|
allocation.get_width(), allocation.get_height(), allocation.get_width() * 4);
|
2015-03-19 01:02:11 +08:00
|
|
|
cr->set_source(surface, 0, 0);
|
|
|
|
cr->paint();
|
2015-03-20 20:47:55 +08:00
|
|
|
surface->finish();
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_gl_draw() = 0;
|
|
|
|
|
|
|
|
private:
|
|
|
|
Display *_xdisplay;
|
2015-03-20 20:47:55 +08:00
|
|
|
GLXContext _glcontext;
|
2015-03-21 03:35:04 +08:00
|
|
|
GLOffscreen *_offscreen;
|
2015-03-21 06:59:48 +08:00
|
|
|
::Window _xwindow;
|
2015-03-19 01:02:11 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Editor overlay */
|
|
|
|
|
|
|
|
class EditorOverlay : public Gtk::Fixed {
|
|
|
|
public:
|
|
|
|
EditorOverlay(Gtk::Widget &underlay) : _underlay(underlay) {
|
2016-05-21 07:39:06 +08:00
|
|
|
set_size_request(0, 0);
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
add(_underlay);
|
|
|
|
|
|
|
|
_entry.set_no_show_all(true);
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
_entry.set_has_frame(false);
|
2015-03-19 01:02:11 +08:00
|
|
|
add(_entry);
|
|
|
|
|
|
|
|
_entry.signal_activate().
|
|
|
|
connect(sigc::mem_fun(this, &EditorOverlay::on_activate));
|
|
|
|
}
|
|
|
|
|
2016-04-16 08:10:32 +08:00
|
|
|
void start_editing(int x, int y, int font_height, bool is_monospace, int minWidthChars,
|
|
|
|
const std::string &val) {
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
Pango::FontDescription font_desc;
|
|
|
|
font_desc.set_family(is_monospace ? "monospace" : "normal");
|
|
|
|
font_desc.set_absolute_size(font_height * Pango::SCALE);
|
|
|
|
|
|
|
|
#ifdef HAVE_GTK3
|
2016-04-18 14:21:35 +08:00
|
|
|
/* For some reason override_font doesn't take screen DPI into
|
|
|
|
account on GTK3 when working with font descriptors specified
|
|
|
|
in absolute sizes; modify_font does on GTK2. */
|
|
|
|
Pango::FontDescription override_font_desc(font_desc);
|
|
|
|
double dpi = get_screen()->get_resolution();
|
|
|
|
override_font_desc.set_size(font_height * 72.0 / dpi * Pango::SCALE);
|
|
|
|
_entry.override_font(override_font_desc);
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
#else
|
|
|
|
_entry.modify_font(font_desc);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* y coordinate denotes baseline */
|
|
|
|
Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc);
|
|
|
|
y -= font_metrics.get_ascent() / Pango::SCALE;
|
|
|
|
|
2016-04-16 08:10:32 +08:00
|
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
|
|
|
|
layout->set_font_description(font_desc);
|
|
|
|
layout->set_text(val + " "); /* avoid scrolling */
|
2016-04-18 14:21:35 +08:00
|
|
|
int width = layout->get_logical_extents().get_width();
|
2016-04-16 08:10:32 +08:00
|
|
|
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
Gtk::Border border = _entry.get_style_context()->get_padding();
|
|
|
|
move(_entry, x - border.get_left(), y - border.get_top());
|
2016-04-18 14:21:35 +08:00
|
|
|
_entry.set_width_chars(minWidthChars);
|
|
|
|
_entry.set_size_request(width / Pango::SCALE, -1);
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
#else
|
|
|
|
/* We need _gtk_entry_effective_inner_border, but it's not
|
|
|
|
in the public API, so emulate its logic. */
|
|
|
|
Gtk::Border border = { 2, 2, 2, 2 }, *style_border;
|
|
|
|
gtk_widget_style_get(GTK_WIDGET(_entry.gobj()), "inner-border",
|
|
|
|
&style_border, NULL);
|
|
|
|
if(style_border) border = *style_border;
|
|
|
|
move(_entry, x - border.left, y - border.top);
|
2016-04-18 14:21:35 +08:00
|
|
|
/* This is what set_width_chars does. */
|
|
|
|
int minWidth = minWidthChars * std::max(font_metrics.get_approximate_digit_width(),
|
|
|
|
font_metrics.get_approximate_char_width());
|
|
|
|
_entry.set_size_request(std::max(width, minWidth) / Pango::SCALE, -1);
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
#endif
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
_entry.set_text(val);
|
|
|
|
if(!_entry.is_visible()) {
|
|
|
|
_entry.show();
|
|
|
|
_entry.grab_focus();
|
|
|
|
_entry.add_modal_grab();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void stop_editing() {
|
|
|
|
if(_entry.is_visible())
|
|
|
|
_entry.remove_modal_grab();
|
|
|
|
_entry.hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool is_editing() const {
|
|
|
|
return _entry.is_visible();
|
|
|
|
}
|
|
|
|
|
|
|
|
sigc::signal<void, Glib::ustring> signal_editing_done() {
|
|
|
|
return _signal_editing_done;
|
|
|
|
}
|
|
|
|
|
|
|
|
Gtk::Entry &get_entry() {
|
|
|
|
return _entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_key_press_event(GdkEventKey *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
if(event->keyval == GDK_KEY_Escape) {
|
|
|
|
stop_editing();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_size_allocate(Gtk::Allocation& allocation) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::Fixed::on_size_allocate(allocation);
|
|
|
|
|
|
|
|
_underlay.size_allocate(allocation);
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_activate() {
|
2015-03-19 01:02:11 +08:00
|
|
|
_signal_editing_done(_entry.get_text());
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Gtk::Widget &_underlay;
|
|
|
|
Gtk::Entry _entry;
|
|
|
|
sigc::signal<void, Glib::ustring> _signal_editing_done;
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Graphics window */
|
|
|
|
|
|
|
|
int DeltaYOfScrollEvent(GdkEventScroll *event) {
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
int delta_y = event->delta_y;
|
|
|
|
#else
|
|
|
|
int delta_y = 0;
|
|
|
|
#endif
|
|
|
|
if(delta_y == 0) {
|
|
|
|
switch(event->direction) {
|
|
|
|
case GDK_SCROLL_UP:
|
|
|
|
delta_y = -1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GDK_SCROLL_DOWN:
|
|
|
|
delta_y = 1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
/* do nothing */
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return delta_y;
|
|
|
|
}
|
|
|
|
|
|
|
|
class GraphicsWidget : public GlWidget {
|
|
|
|
public:
|
|
|
|
GraphicsWidget() {
|
|
|
|
set_events(Gdk::POINTER_MOTION_MASK |
|
|
|
|
Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK |
|
|
|
|
Gdk::SCROLL_MASK |
|
|
|
|
Gdk::LEAVE_NOTIFY_MASK);
|
|
|
|
set_double_buffered(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_configure_event(GdkEventConfigure *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
_w = event->width;
|
|
|
|
_h = event->height;
|
|
|
|
|
|
|
|
return GlWidget::on_configure_event(event);;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_gl_draw() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.GW.Paint();
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_motion_notify_event(GdkEventMotion *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
int x, y;
|
|
|
|
ij_to_xy(event->x, event->y, x, y);
|
|
|
|
|
|
|
|
SS.GW.MouseMoved(x, y,
|
|
|
|
event->state & GDK_BUTTON1_MASK,
|
|
|
|
event->state & GDK_BUTTON2_MASK,
|
|
|
|
event->state & GDK_BUTTON3_MASK,
|
|
|
|
event->state & GDK_SHIFT_MASK,
|
|
|
|
event->state & GDK_CONTROL_MASK);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_button_press_event(GdkEventButton *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
int x, y;
|
|
|
|
ij_to_xy(event->x, event->y, x, y);
|
|
|
|
|
|
|
|
switch(event->button) {
|
|
|
|
case 1:
|
|
|
|
if(event->type == GDK_BUTTON_PRESS)
|
|
|
|
SS.GW.MouseLeftDown(x, y);
|
|
|
|
else if(event->type == GDK_2BUTTON_PRESS)
|
|
|
|
SS.GW.MouseLeftDoubleClick(x, y);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 2:
|
|
|
|
case 3:
|
|
|
|
SS.GW.MouseMiddleOrRightDown(x, y);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_button_release_event(GdkEventButton *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
int x, y;
|
|
|
|
ij_to_xy(event->x, event->y, x, y);
|
|
|
|
|
|
|
|
switch(event->button) {
|
|
|
|
case 1:
|
|
|
|
SS.GW.MouseLeftUp(x, y);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 3:
|
|
|
|
SS.GW.MouseRightUp(x, y);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_scroll_event(GdkEventScroll *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
int x, y;
|
|
|
|
ij_to_xy(event->x, event->y, x, y);
|
|
|
|
|
|
|
|
SS.GW.MouseScroll(x, y, -DeltaYOfScrollEvent(event));
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_leave_notify_event (GdkEventCrossing *) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.GW.MouseLeave();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
int _w, _h;
|
2016-05-18 17:40:50 +08:00
|
|
|
void ij_to_xy(double i, double j, int &x, int &y) {
|
2015-03-19 01:02:11 +08:00
|
|
|
// Convert to xy (vs. ij) style coordinates,
|
|
|
|
// with (0, 0) at center
|
2016-05-18 17:40:50 +08:00
|
|
|
x = (int)i - _w / 2;
|
|
|
|
y = _h / 2 - (int)j;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
class GraphicsWindowGtk : public Gtk::Window {
|
2015-03-19 01:02:11 +08:00
|
|
|
public:
|
2016-04-24 08:19:04 +08:00
|
|
|
GraphicsWindowGtk() : _overlay(_widget), _is_fullscreen(false) {
|
2015-03-19 01:02:11 +08:00
|
|
|
set_default_size(900, 600);
|
|
|
|
|
|
|
|
_box.pack_start(_menubar, false, true);
|
|
|
|
_box.pack_start(_overlay, true, true);
|
|
|
|
|
|
|
|
add(_box);
|
|
|
|
|
|
|
|
_overlay.signal_editing_done().
|
2015-03-24 01:49:04 +08:00
|
|
|
connect(sigc::mem_fun(this, &GraphicsWindowGtk::on_editing_done));
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
GraphicsWidget &get_widget() {
|
|
|
|
return _widget;
|
|
|
|
}
|
|
|
|
|
|
|
|
EditorOverlay &get_overlay() {
|
|
|
|
return _overlay;
|
|
|
|
}
|
|
|
|
|
|
|
|
Gtk::MenuBar &get_menubar() {
|
|
|
|
return _menubar;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool is_fullscreen() const {
|
|
|
|
return _is_fullscreen;
|
|
|
|
}
|
|
|
|
|
2016-05-18 20:14:42 +08:00
|
|
|
bool emulate_key_press(GdkEventKey *event) {
|
|
|
|
return on_key_press_event(event);
|
|
|
|
}
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_show() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::Window::on_show();
|
|
|
|
|
|
|
|
CnfThawWindowPos(this, "GraphicsWindow");
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_hide() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
CnfFreezeWindowPos(this, "GraphicsWindow");
|
|
|
|
|
|
|
|
Gtk::Window::on_hide();
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_delete_event(GdkEventAny *) override {
|
2016-05-21 05:48:10 +08:00
|
|
|
if(!SS.OkayToStartNewFile()) return true;
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.Exit();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_window_state_event(GdkEventWindowState *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
_is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
|
|
|
|
|
|
|
|
/* The event arrives too late for the caller of ToggleFullScreen
|
|
|
|
to notice state change; and it's possible that the WM will
|
|
|
|
refuse our request, so we can't just toggle the saved state */
|
|
|
|
SS.GW.EnsureValidActives();
|
|
|
|
|
|
|
|
return Gtk::Window::on_window_state_event(event);
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_key_press_event(GdkEventKey *event) override {
|
2016-05-18 20:14:42 +08:00
|
|
|
int chr;
|
|
|
|
|
|
|
|
switch(event->keyval) {
|
|
|
|
case GDK_KEY_Escape:
|
|
|
|
chr = GraphicsWindow::ESCAPE_KEY;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GDK_KEY_Delete:
|
|
|
|
chr = GraphicsWindow::DELETE_KEY;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GDK_KEY_Tab:
|
|
|
|
chr = '\t';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GDK_KEY_BackSpace:
|
|
|
|
case GDK_KEY_Back:
|
|
|
|
chr = '\b';
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
if(event->keyval >= GDK_KEY_F1 && event->keyval <= GDK_KEY_F12) {
|
|
|
|
chr = GraphicsWindow::FUNCTION_KEY_BASE + (event->keyval - GDK_KEY_F1);
|
|
|
|
} else {
|
|
|
|
chr = gdk_keyval_to_unicode(event->keyval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(event->state & GDK_SHIFT_MASK){
|
|
|
|
chr |= GraphicsWindow::SHIFT_MASK;
|
|
|
|
}
|
|
|
|
if(event->state & GDK_CONTROL_MASK) {
|
|
|
|
chr |= GraphicsWindow::CTRL_MASK;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(chr && SS.GW.KeyDown(chr)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(chr == '\t') {
|
|
|
|
// Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=123994.
|
|
|
|
GraphicsWindow::MenuView(GraphicsWindow::MNU_SHOW_TEXT_WND);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Gtk::Window::on_key_press_event(event);
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_editing_done(Glib::ustring value) {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.GW.EditControlDone(value.c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
GraphicsWidget _widget;
|
|
|
|
EditorOverlay _overlay;
|
|
|
|
Gtk::MenuBar _menubar;
|
|
|
|
Gtk::VBox _box;
|
|
|
|
|
|
|
|
bool _is_fullscreen;
|
|
|
|
};
|
|
|
|
|
2016-05-10 07:13:49 +08:00
|
|
|
std::unique_ptr<GraphicsWindowGtk> GW;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
void GetGraphicsWindowSize(int *w, int *h) {
|
|
|
|
Gdk::Rectangle allocation = GW->get_widget().get_allocation();
|
|
|
|
*w = allocation.get_width();
|
|
|
|
*h = allocation.get_height();
|
|
|
|
}
|
|
|
|
|
|
|
|
void InvalidateGraphics(void) {
|
|
|
|
GW->get_widget().queue_draw();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PaintGraphics(void) {
|
|
|
|
GW->get_widget().queue_draw();
|
|
|
|
/* Process animation */
|
|
|
|
Glib::MainContext::get_default()->iteration(false);
|
|
|
|
}
|
|
|
|
|
2015-12-27 09:03:24 +08:00
|
|
|
void SetCurrentFilename(const std::string &filename) {
|
|
|
|
if(!filename.empty()) {
|
|
|
|
GW->set_title("SolveSpace - " + filename);
|
2015-03-24 14:45:53 +08:00
|
|
|
} else {
|
|
|
|
GW->set_title("SolveSpace - (not yet saved)");
|
|
|
|
}
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void ToggleFullScreen(void) {
|
|
|
|
if(GW->is_fullscreen())
|
|
|
|
GW->unfullscreen();
|
|
|
|
else
|
|
|
|
GW->fullscreen();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool FullScreenIsActive(void) {
|
|
|
|
return GW->is_fullscreen();
|
|
|
|
}
|
|
|
|
|
2016-04-16 08:10:32 +08:00
|
|
|
void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars,
|
|
|
|
const std::string &val) {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gdk::Rectangle rect = GW->get_widget().get_allocation();
|
|
|
|
|
|
|
|
// Convert to ij (vs. xy) style coordinates,
|
|
|
|
// and compensate for the input widget height due to inverse coord
|
|
|
|
int i, j;
|
|
|
|
i = x + rect.get_width() / 2;
|
Ensure edit control font size matches font size of text being edited.
Before this commit, the position of the edit box was adjusted
by trial and error, as far as I can tell. This commit changes
the positioning machinery for edit controls as follows:
The coordinates passed to ShowTextEditControl/ShowGraphicsEditControl
now denote: X the left bound, and Y the baseline.
The font height passed to ShowGraphicsEditControl denotes
the absolute font height in pixels, i.e. ascent plus descent.
Platform-dependent code uses these coordinates, the font metrics
for the font appropriate for the platform, and the knowledge of
the decorations drawn around the text by the native edit control
to position the edit control in a way that overlays the text inside
the edit control with the rendered text.
On OS X, GNU Unifont (of height 16) has metrics identical to
Monaco (of height 15) and so as an exception, the edit control
is nudged slightly for a pixel-perfect fit.
Also, since the built-in vector font is proportional, this commit
also switches the edit control font to proportional when editing
constraints.
2016-04-12 22:24:09 +08:00
|
|
|
j = -y + rect.get_height() / 2;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2016-04-16 08:10:32 +08:00
|
|
|
GW->get_overlay().start_editing(i, j, fontHeight, /*is_monospace=*/false, minWidthChars, val);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void HideGraphicsEditControl(void) {
|
|
|
|
GW->get_overlay().stop_editing();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GraphicsEditControlIsVisible(void) {
|
|
|
|
return GW->get_overlay().is_editing();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* TODO: removing menubar breaks accelerators. */
|
|
|
|
void ToggleMenuBar(void) {
|
|
|
|
GW->get_menubar().set_visible(!GW->get_menubar().is_visible());
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MenuBarIsVisible(void) {
|
|
|
|
return GW->get_menubar().is_visible();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Context menus */
|
|
|
|
|
|
|
|
class ContextMenuItem : public Gtk::MenuItem {
|
|
|
|
public:
|
|
|
|
static int choice;
|
|
|
|
|
|
|
|
ContextMenuItem(const Glib::ustring &label, int id, bool mnemonic=false) :
|
|
|
|
Gtk::MenuItem(label, mnemonic), _id(id) {
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_activate() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::MenuItem::on_activate();
|
|
|
|
|
|
|
|
if(has_submenu())
|
|
|
|
return;
|
|
|
|
|
|
|
|
choice = _id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=695488.
|
|
|
|
This is used in addition to on_activate() to catch mouse events.
|
|
|
|
Without on_activate(), it would be impossible to select a menu item
|
|
|
|
via keyboard.
|
|
|
|
This selects the item twice in some cases, but we are idempotent.
|
|
|
|
*/
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_button_press_event(GdkEventButton *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
if(event->button == 1 && event->type == GDK_BUTTON_PRESS) {
|
|
|
|
on_activate();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Gtk::MenuItem::on_button_press_event(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
int _id;
|
|
|
|
};
|
|
|
|
|
|
|
|
int ContextMenuItem::choice = 0;
|
|
|
|
|
|
|
|
static Gtk::Menu *context_menu = NULL, *context_submenu = NULL;
|
|
|
|
|
|
|
|
void AddContextMenuItem(const char *label, int id) {
|
|
|
|
Gtk::MenuItem *menu_item;
|
|
|
|
if(label)
|
2015-03-24 01:49:04 +08:00
|
|
|
menu_item = new ContextMenuItem(label, id);
|
2015-03-19 01:02:11 +08:00
|
|
|
else
|
|
|
|
menu_item = new Gtk::SeparatorMenuItem();
|
|
|
|
|
|
|
|
if(id == CONTEXT_SUBMENU) {
|
|
|
|
menu_item->set_submenu(*context_submenu);
|
|
|
|
context_submenu = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(context_submenu) {
|
|
|
|
context_submenu->append(*menu_item);
|
|
|
|
} else {
|
|
|
|
if(!context_menu)
|
|
|
|
context_menu = new Gtk::Menu;
|
|
|
|
|
|
|
|
context_menu->append(*menu_item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CreateContextSubmenu(void) {
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(!context_submenu, "Unexpected nested submenu");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
context_submenu = new Gtk::Menu;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ShowContextMenu(void) {
|
|
|
|
if(!context_menu)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
Glib::RefPtr<Glib::MainLoop> loop = Glib::MainLoop::create();
|
|
|
|
context_menu->signal_deactivate().
|
|
|
|
connect(sigc::mem_fun(loop.operator->(), &Glib::MainLoop::quit));
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
ContextMenuItem::choice = -1;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
context_menu->show_all();
|
|
|
|
context_menu->popup(3, GDK_CURRENT_TIME);
|
|
|
|
|
|
|
|
loop->run();
|
|
|
|
|
|
|
|
delete context_menu;
|
|
|
|
context_menu = NULL;
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
return ContextMenuItem::choice;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Main menu */
|
|
|
|
|
|
|
|
template<class MenuItem> class MainMenuItem : public MenuItem {
|
|
|
|
public:
|
2015-03-24 01:49:04 +08:00
|
|
|
MainMenuItem(const GraphicsWindow::MenuEntry &entry) :
|
2015-03-19 01:02:11 +08:00
|
|
|
MenuItem(), _entry(entry), _synthetic(false) {
|
|
|
|
Glib::ustring label(_entry.label);
|
2016-05-08 07:34:21 +08:00
|
|
|
for(size_t i = 0; i < label.length(); i++) {
|
2015-03-19 01:02:11 +08:00
|
|
|
if(label[i] == '&')
|
|
|
|
label.replace(i, 1, "_");
|
|
|
|
}
|
|
|
|
|
|
|
|
guint accel_key = 0;
|
|
|
|
Gdk::ModifierType accel_mods = Gdk::ModifierType();
|
|
|
|
switch(_entry.accel) {
|
2015-03-24 01:49:04 +08:00
|
|
|
case GraphicsWindow::DELETE_KEY:
|
2015-03-19 01:02:11 +08:00
|
|
|
accel_key = GDK_KEY_Delete;
|
|
|
|
break;
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
case GraphicsWindow::ESCAPE_KEY:
|
2015-03-19 01:02:11 +08:00
|
|
|
accel_key = GDK_KEY_Escape;
|
|
|
|
break;
|
|
|
|
|
2016-05-18 20:14:42 +08:00
|
|
|
case '\t':
|
|
|
|
accel_key = GDK_KEY_Tab;
|
|
|
|
break;
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
default:
|
2015-03-24 01:49:04 +08:00
|
|
|
accel_key = _entry.accel & ~(GraphicsWindow::SHIFT_MASK | GraphicsWindow::CTRL_MASK);
|
|
|
|
if(accel_key > GraphicsWindow::FUNCTION_KEY_BASE &&
|
|
|
|
accel_key <= GraphicsWindow::FUNCTION_KEY_BASE + 12)
|
|
|
|
accel_key = GDK_KEY_F1 + (accel_key - GraphicsWindow::FUNCTION_KEY_BASE - 1);
|
2015-03-19 01:02:11 +08:00
|
|
|
else
|
|
|
|
accel_key = gdk_unicode_to_keyval(accel_key);
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
if(_entry.accel & GraphicsWindow::SHIFT_MASK)
|
2015-03-19 01:02:11 +08:00
|
|
|
accel_mods |= Gdk::SHIFT_MASK;
|
2015-03-24 01:49:04 +08:00
|
|
|
if(_entry.accel & GraphicsWindow::CTRL_MASK)
|
2015-03-19 01:02:11 +08:00
|
|
|
accel_mods |= Gdk::CONTROL_MASK;
|
|
|
|
}
|
|
|
|
|
|
|
|
MenuItem::set_label(label);
|
|
|
|
MenuItem::set_use_underline(true);
|
|
|
|
if(!(accel_key & 0x01000000))
|
|
|
|
MenuItem::set_accel_key(Gtk::AccelKey(accel_key, accel_mods));
|
|
|
|
}
|
|
|
|
|
|
|
|
void set_active(bool checked) {
|
|
|
|
if(MenuItem::get_active() == checked)
|
|
|
|
return;
|
|
|
|
|
|
|
|
_synthetic = true;
|
|
|
|
MenuItem::set_active(checked);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_activate() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
MenuItem::on_activate();
|
|
|
|
|
|
|
|
if(_synthetic)
|
|
|
|
_synthetic = false;
|
|
|
|
else if(!MenuItem::has_submenu() && _entry.fn)
|
|
|
|
_entry.fn(_entry.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2015-03-24 01:49:04 +08:00
|
|
|
const GraphicsWindow::MenuEntry &_entry;
|
2015-03-19 01:02:11 +08:00
|
|
|
bool _synthetic;
|
|
|
|
};
|
|
|
|
|
|
|
|
static std::map<int, Gtk::MenuItem *> main_menu_items;
|
|
|
|
|
|
|
|
static void InitMainMenu(Gtk::MenuShell *menu_shell) {
|
|
|
|
Gtk::MenuItem *menu_item = NULL;
|
|
|
|
Gtk::MenuShell *levels[5] = {menu_shell, 0};
|
|
|
|
|
|
|
|
const GraphicsWindow::MenuEntry *entry = &GraphicsWindow::menu[0];
|
|
|
|
int current_level = 0;
|
|
|
|
while(entry->level >= 0) {
|
|
|
|
if(entry->level > current_level) {
|
|
|
|
Gtk::Menu *menu = new Gtk::Menu;
|
|
|
|
menu_item->set_submenu(*menu);
|
|
|
|
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert((unsigned)entry->level < sizeof(levels) / sizeof(levels[0]),
|
|
|
|
"Unexpected depth of menu nesting");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
levels[entry->level] = menu;
|
|
|
|
}
|
|
|
|
|
|
|
|
current_level = entry->level;
|
|
|
|
|
|
|
|
if(entry->label) {
|
|
|
|
switch(entry->kind) {
|
|
|
|
case GraphicsWindow::MENU_ITEM_NORMAL:
|
2015-03-24 01:49:04 +08:00
|
|
|
menu_item = new MainMenuItem<Gtk::MenuItem>(*entry);
|
2015-03-19 01:02:11 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case GraphicsWindow::MENU_ITEM_CHECK:
|
2015-03-24 01:49:04 +08:00
|
|
|
menu_item = new MainMenuItem<Gtk::CheckMenuItem>(*entry);
|
2015-03-19 01:02:11 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case GraphicsWindow::MENU_ITEM_RADIO:
|
2015-03-24 01:49:04 +08:00
|
|
|
MainMenuItem<Gtk::CheckMenuItem> *radio_item =
|
|
|
|
new MainMenuItem<Gtk::CheckMenuItem>(*entry);
|
2015-03-19 01:02:11 +08:00
|
|
|
radio_item->set_draw_as_radio(true);
|
|
|
|
menu_item = radio_item;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
menu_item = new Gtk::SeparatorMenuItem();
|
|
|
|
}
|
|
|
|
|
|
|
|
levels[entry->level]->append(*menu_item);
|
|
|
|
|
|
|
|
main_menu_items[entry->id] = menu_item;
|
|
|
|
|
|
|
|
++entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void EnableMenuById(int id, bool enabled) {
|
|
|
|
main_menu_items[id]->set_sensitive(enabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
void CheckMenuById(int id, bool checked) {
|
2015-03-24 01:49:04 +08:00
|
|
|
((MainMenuItem<Gtk::CheckMenuItem>*)main_menu_items[id])->set_active(checked);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void RadioMenuById(int id, bool selected) {
|
2015-03-24 01:49:04 +08:00
|
|
|
SolveSpace::CheckMenuById(id, selected);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
class RecentMenuItem : public Gtk::MenuItem {
|
|
|
|
public:
|
|
|
|
RecentMenuItem(const Glib::ustring& label, int id) :
|
|
|
|
MenuItem(label), _id(id) {
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_activate() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
if(_id >= RECENT_OPEN && _id < (RECENT_OPEN + MAX_RECENT))
|
2015-03-24 01:49:04 +08:00
|
|
|
SolveSpaceUI::MenuFile(_id);
|
2016-05-07 13:27:54 +08:00
|
|
|
else if(_id >= RECENT_LINK && _id < (RECENT_LINK + MAX_RECENT))
|
2015-03-19 01:02:11 +08:00
|
|
|
Group::MenuGroup(_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
int _id;
|
|
|
|
};
|
|
|
|
|
|
|
|
static void RefreshRecentMenu(int id, int base) {
|
|
|
|
Gtk::MenuItem *recent = static_cast<Gtk::MenuItem*>(main_menu_items[id]);
|
|
|
|
recent->unset_submenu();
|
|
|
|
|
|
|
|
Gtk::Menu *menu = new Gtk::Menu;
|
|
|
|
recent->set_submenu(*menu);
|
|
|
|
|
|
|
|
if(std::string(RecentFile[0]).empty()) {
|
|
|
|
Gtk::MenuItem *placeholder = new Gtk::MenuItem("(no recent files)");
|
|
|
|
placeholder->set_sensitive(false);
|
|
|
|
menu->append(*placeholder);
|
|
|
|
} else {
|
|
|
|
for(int i = 0; i < MAX_RECENT; i++) {
|
|
|
|
if(std::string(RecentFile[i]).empty())
|
|
|
|
break;
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
RecentMenuItem *item = new RecentMenuItem(RecentFile[i], base + i);
|
2015-03-19 01:02:11 +08:00
|
|
|
menu->append(*item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
menu->show_all();
|
|
|
|
}
|
|
|
|
|
|
|
|
void RefreshRecentMenus(void) {
|
|
|
|
RefreshRecentMenu(GraphicsWindow::MNU_OPEN_RECENT, RECENT_OPEN);
|
2016-05-07 13:27:54 +08:00
|
|
|
RefreshRecentMenu(GraphicsWindow::MNU_GROUP_RECENT, RECENT_LINK);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Save/load */
|
|
|
|
|
2016-05-04 11:12:06 +08:00
|
|
|
static std::string ConvertFilters(std::string active, const FileFilter ssFilters[],
|
|
|
|
Gtk::FileChooser *chooser) {
|
|
|
|
for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) {
|
2015-03-19 01:02:11 +08:00
|
|
|
#ifdef HAVE_GTK3
|
2016-05-04 11:12:06 +08:00
|
|
|
Glib::RefPtr<Gtk::FileFilter> filter = Gtk::FileFilter::create();
|
2015-03-19 01:02:11 +08:00
|
|
|
#else
|
2016-05-04 11:12:06 +08:00
|
|
|
Gtk::FileFilter *filter = new Gtk::FileFilter;
|
2015-03-19 01:02:11 +08:00
|
|
|
#endif
|
2016-05-04 11:12:06 +08:00
|
|
|
filter->set_name(ssFilter->name);
|
|
|
|
|
|
|
|
bool is_active = false;
|
|
|
|
std::string desc = "";
|
|
|
|
for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) {
|
|
|
|
std::string pattern = "*." + std::string(*ssPattern);
|
|
|
|
filter->add_pattern(pattern);
|
|
|
|
if(active == "")
|
|
|
|
active = pattern.substr(2);
|
|
|
|
if("*." + active == pattern)
|
|
|
|
is_active = true;
|
|
|
|
if(desc == "")
|
|
|
|
desc = pattern;
|
|
|
|
else
|
|
|
|
desc += ", " + pattern;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
2016-05-04 11:12:06 +08:00
|
|
|
filter->set_name(filter->get_name() + " (" + desc + ")");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2016-05-10 21:10:41 +08:00
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
chooser->add_filter(filter);
|
|
|
|
if(is_active)
|
|
|
|
chooser->set_filter(filter);
|
|
|
|
#else
|
2016-05-04 11:12:06 +08:00
|
|
|
chooser->add_filter(*filter);
|
|
|
|
if(is_active)
|
|
|
|
chooser->set_filter(*filter);
|
2016-05-10 21:10:41 +08:00
|
|
|
#endif
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
2016-01-11 16:44:56 +08:00
|
|
|
|
2016-05-04 11:12:06 +08:00
|
|
|
return active;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
2016-05-04 11:12:06 +08:00
|
|
|
bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty,
|
|
|
|
const FileFilter filters[]) {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Open File");
|
2016-05-04 11:12:06 +08:00
|
|
|
chooser.set_filename(*filename);
|
2015-03-19 01:02:11 +08:00
|
|
|
chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
|
|
|
|
chooser.add_button("_Open", Gtk::RESPONSE_OK);
|
2015-12-26 23:54:26 +08:00
|
|
|
chooser.set_current_folder(CnfThawString("", "FileChooserPath"));
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2016-05-04 11:12:06 +08:00
|
|
|
ConvertFilters(activeOrEmpty, filters, &chooser);
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
if(chooser.run() == Gtk::RESPONSE_OK) {
|
2015-12-27 09:03:24 +08:00
|
|
|
CnfFreezeString(chooser.get_current_folder(), "FileChooserPath");
|
2016-05-04 11:12:06 +08:00
|
|
|
*filename = chooser.get_filename();
|
2015-03-19 01:02:11 +08:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Glib::path_get_basename got /removed/ in 3.0?! Come on */
|
|
|
|
static std::string Basename(std::string filename) {
|
|
|
|
int slash = filename.rfind('/');
|
|
|
|
if(slash >= 0)
|
|
|
|
return filename.substr(slash + 1, filename.length());
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser)
|
|
|
|
{
|
|
|
|
/* Extract the pattern from the filter. GtkFileFilter doesn't provide
|
|
|
|
any way to list the patterns, so we extract it from the filter name.
|
|
|
|
Gross. */
|
|
|
|
std::string filter_name = chooser->get_filter()->get_name();
|
2016-03-25 19:08:04 +08:00
|
|
|
int lparen = filter_name.rfind('(') + 1;
|
2015-03-19 01:02:11 +08:00
|
|
|
int rdelim = filter_name.find(',', lparen);
|
|
|
|
if(rdelim < 0)
|
|
|
|
rdelim = filter_name.find(')', lparen);
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(lparen > 0 && rdelim > 0, "Expected to find a parenthesized extension");
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
std::string extension = filter_name.substr(lparen, rdelim - lparen);
|
|
|
|
if(extension == "*")
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(extension.length() > 2 && extension.substr(0, 2) == "*.")
|
|
|
|
extension = extension.substr(2, extension.length() - 2);
|
|
|
|
|
|
|
|
std::string basename = Basename(chooser->get_filename());
|
|
|
|
int dot = basename.rfind('.');
|
|
|
|
if(dot >= 0) {
|
|
|
|
basename.replace(dot + 1, basename.length() - dot - 1, extension);
|
|
|
|
chooser->set_current_name(basename);
|
|
|
|
} else {
|
|
|
|
chooser->set_current_name(basename + "." + extension);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-04 11:12:06 +08:00
|
|
|
bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty,
|
|
|
|
const FileFilter filters[]) {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Save File",
|
|
|
|
Gtk::FILE_CHOOSER_ACTION_SAVE);
|
|
|
|
chooser.set_do_overwrite_confirmation(true);
|
|
|
|
chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
|
|
|
|
chooser.add_button("_Save", Gtk::RESPONSE_OK);
|
|
|
|
|
2016-05-04 11:12:06 +08:00
|
|
|
std::string active = ConvertFilters(activeOrEmpty, filters, &chooser);
|
2015-03-19 01:02:11 +08:00
|
|
|
|
2015-12-26 23:54:26 +08:00
|
|
|
chooser.set_current_folder(CnfThawString("", "FileChooserPath"));
|
2015-03-19 01:02:11 +08:00
|
|
|
chooser.set_current_name(std::string("untitled.") + active);
|
|
|
|
|
|
|
|
/* Gtk's dialog doesn't change the extension when you change the filter,
|
|
|
|
and makes it extremely hard to do so. Gtk is garbage. */
|
|
|
|
chooser.property_filter().signal_changed().
|
|
|
|
connect(sigc::bind(sigc::ptr_fun(&ChooserFilterChanged), &chooser));
|
|
|
|
|
|
|
|
if(chooser.run() == Gtk::RESPONSE_OK) {
|
2015-12-26 23:54:26 +08:00
|
|
|
CnfFreezeString(chooser.get_current_folder(), "FileChooserPath");
|
2016-05-04 11:12:06 +08:00
|
|
|
*filename = chooser.get_filename();
|
2015-03-19 01:02:11 +08:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-11 20:18:18 +08:00
|
|
|
DialogChoice SaveFileYesNoCancel(void) {
|
2015-03-19 01:02:11 +08:00
|
|
|
Glib::ustring message =
|
|
|
|
"The file has changed since it was last saved.\n"
|
|
|
|
"Do you want to save the changes?";
|
|
|
|
Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION,
|
|
|
|
Gtk::BUTTONS_NONE, /*is_modal*/ true);
|
|
|
|
dialog.set_title("SolveSpace - Modified File");
|
|
|
|
dialog.add_button("_Save", Gtk::RESPONSE_YES);
|
2015-03-29 12:46:57 +08:00
|
|
|
dialog.add_button("Do_n't Save", Gtk::RESPONSE_NO);
|
2015-03-19 01:02:11 +08:00
|
|
|
dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
|
|
|
|
|
|
|
|
switch(dialog.run()) {
|
|
|
|
case Gtk::RESPONSE_YES:
|
2016-01-11 20:18:18 +08:00
|
|
|
return DIALOG_YES;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
case Gtk::RESPONSE_NO:
|
2016-01-11 20:18:18 +08:00
|
|
|
return DIALOG_NO;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
case Gtk::RESPONSE_CANCEL:
|
|
|
|
default:
|
2016-01-11 20:18:18 +08:00
|
|
|
return DIALOG_CANCEL;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-11 20:18:18 +08:00
|
|
|
DialogChoice LoadAutosaveYesNo(void) {
|
2015-03-29 12:46:57 +08:00
|
|
|
Glib::ustring message =
|
|
|
|
"An autosave file is availible for this project.\n"
|
|
|
|
"Do you want to load the autosave file instead?";
|
|
|
|
Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION,
|
|
|
|
Gtk::BUTTONS_NONE, /*is_modal*/ true);
|
|
|
|
dialog.set_title("SolveSpace - Autosave Available");
|
|
|
|
dialog.add_button("_Load autosave", Gtk::RESPONSE_YES);
|
|
|
|
dialog.add_button("Do_n't Load", Gtk::RESPONSE_NO);
|
|
|
|
|
|
|
|
switch(dialog.run()) {
|
|
|
|
case Gtk::RESPONSE_YES:
|
2016-01-11 20:18:18 +08:00
|
|
|
return DIALOG_YES;
|
2015-03-29 12:46:57 +08:00
|
|
|
|
|
|
|
case Gtk::RESPONSE_NO:
|
|
|
|
default:
|
2016-01-11 20:18:18 +08:00
|
|
|
return DIALOG_NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
DialogChoice LocateImportedFileYesNoCancel(const std::string &filename,
|
|
|
|
bool canCancel) {
|
|
|
|
Glib::ustring message =
|
2016-05-07 13:27:54 +08:00
|
|
|
"The linked file " + filename + " is not present.\n"
|
2016-01-11 20:18:18 +08:00
|
|
|
"Do you want to locate it manually?\n"
|
|
|
|
"If you select \"No\", any geometry that depends on "
|
|
|
|
"the missing file will be removed.";
|
|
|
|
Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION,
|
|
|
|
Gtk::BUTTONS_NONE, /*is_modal*/ true);
|
|
|
|
dialog.set_title("SolveSpace - Missing File");
|
|
|
|
dialog.add_button("_Yes", Gtk::RESPONSE_YES);
|
|
|
|
dialog.add_button("_No", Gtk::RESPONSE_NO);
|
|
|
|
if(canCancel)
|
|
|
|
dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
|
|
|
|
|
|
|
|
switch(dialog.run()) {
|
|
|
|
case Gtk::RESPONSE_YES:
|
|
|
|
return DIALOG_YES;
|
|
|
|
|
|
|
|
case Gtk::RESPONSE_NO:
|
|
|
|
return DIALOG_NO;
|
|
|
|
|
|
|
|
case Gtk::RESPONSE_CANCEL:
|
|
|
|
default:
|
|
|
|
return DIALOG_CANCEL;
|
2015-03-29 12:46:57 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
/* Text window */
|
|
|
|
|
|
|
|
class TextWidget : public GlWidget {
|
|
|
|
public:
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
TextWidget(Glib::RefPtr<Gtk::Adjustment> adjustment) : _adjustment(adjustment) {
|
|
|
|
#else
|
|
|
|
TextWidget(Gtk::Adjustment* adjustment) : _adjustment(adjustment) {
|
|
|
|
#endif
|
|
|
|
set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK |
|
|
|
|
Gdk::LEAVE_NOTIFY_MASK);
|
|
|
|
}
|
|
|
|
|
|
|
|
void set_cursor_hand(bool is_hand) {
|
|
|
|
Glib::RefPtr<Gdk::Window> gdkwin = get_window();
|
|
|
|
if(gdkwin) { // returns NULL if not realized
|
|
|
|
Gdk::CursorType type = is_hand ? Gdk::HAND1 : Gdk::ARROW;
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
gdkwin->set_cursor(Gdk::Cursor::create(type));
|
|
|
|
#else
|
|
|
|
gdkwin->set_cursor(Gdk::Cursor(type));
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_gl_draw() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.TW.Paint();
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_motion_notify_event(GdkEventMotion *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.TW.MouseEvent(/*leftClick*/ false,
|
|
|
|
/*leftDown*/ event->state & GDK_BUTTON1_MASK,
|
|
|
|
event->x, event->y);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_button_press_event(GdkEventButton *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.TW.MouseEvent(/*leftClick*/ event->type == GDK_BUTTON_PRESS,
|
|
|
|
/*leftDown*/ event->state & GDK_BUTTON1_MASK,
|
|
|
|
event->x, event->y);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_scroll_event(GdkEventScroll *event) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
_adjustment->set_value(_adjustment->get_value() +
|
|
|
|
DeltaYOfScrollEvent(event) * _adjustment->get_page_increment());
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_leave_notify_event (GdkEventCrossing *) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.TW.MouseLeave();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
Glib::RefPtr<Gtk::Adjustment> _adjustment;
|
|
|
|
#else
|
|
|
|
Gtk::Adjustment *_adjustment;
|
|
|
|
#endif
|
|
|
|
};
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
class TextWindowGtk : public Gtk::Window {
|
2015-03-19 01:02:11 +08:00
|
|
|
public:
|
2015-03-24 01:49:04 +08:00
|
|
|
TextWindowGtk() : _scrollbar(), _widget(_scrollbar.get_adjustment()),
|
2015-12-28 21:36:31 +08:00
|
|
|
_overlay(_widget), _box() {
|
2015-03-19 01:02:11 +08:00
|
|
|
set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY);
|
|
|
|
set_skip_taskbar_hint(true);
|
|
|
|
set_skip_pager_hint(true);
|
2016-05-18 20:13:59 +08:00
|
|
|
set_title("SolveSpace - Property Browser");
|
2015-03-19 01:02:11 +08:00
|
|
|
set_default_size(420, 300);
|
|
|
|
|
|
|
|
_box.pack_start(_overlay, true, true);
|
|
|
|
_box.pack_start(_scrollbar, false, true);
|
|
|
|
add(_box);
|
|
|
|
|
|
|
|
_scrollbar.get_adjustment()->signal_value_changed().
|
2015-03-24 01:49:04 +08:00
|
|
|
connect(sigc::mem_fun(this, &TextWindowGtk::on_scrollbar_value_changed));
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
_overlay.signal_editing_done().
|
2015-03-24 01:49:04 +08:00
|
|
|
connect(sigc::mem_fun(this, &TextWindowGtk::on_editing_done));
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
_overlay.get_entry().signal_motion_notify_event().
|
2015-03-24 01:49:04 +08:00
|
|
|
connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_motion_notify_event));
|
2015-03-19 01:02:11 +08:00
|
|
|
_overlay.get_entry().signal_button_press_event().
|
2015-03-24 01:49:04 +08:00
|
|
|
connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_button_press_event));
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Gtk::VScrollbar &get_scrollbar() {
|
|
|
|
return _scrollbar;
|
|
|
|
}
|
|
|
|
|
|
|
|
TextWidget &get_widget() {
|
|
|
|
return _widget;
|
|
|
|
}
|
|
|
|
|
|
|
|
EditorOverlay &get_overlay() {
|
|
|
|
return _overlay;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_show() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::Window::on_show();
|
|
|
|
|
|
|
|
CnfThawWindowPos(this, "TextWindow");
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_hide() override {
|
2015-03-19 01:02:11 +08:00
|
|
|
CnfFreezeWindowPos(this, "TextWindow");
|
|
|
|
|
|
|
|
Gtk::Window::on_hide();
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_key_press_event(GdkEventKey *event) override {
|
|
|
|
if(GW->emulate_key_press(event)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Gtk::Window::on_key_press_event(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool on_delete_event(GdkEventAny *) override {
|
2015-03-19 01:02:11 +08:00
|
|
|
/* trigger the action and ignore the request */
|
2015-03-24 01:49:04 +08:00
|
|
|
GraphicsWindow::MenuView(GraphicsWindow::MNU_SHOW_TEXT_WND);
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_scrollbar_value_changed() {
|
2016-05-18 17:40:50 +08:00
|
|
|
SS.TW.ScrollbarEvent((int)_scrollbar.get_adjustment()->get_value());
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
void on_editing_done(Glib::ustring value) {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.TW.EditControlDone(value.c_str());
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_editor_motion_notify_event(GdkEventMotion *event) {
|
2015-03-19 01:02:11 +08:00
|
|
|
return _widget.event((GdkEvent*) event);
|
|
|
|
}
|
|
|
|
|
2016-05-19 02:42:33 +08:00
|
|
|
bool on_editor_button_press_event(GdkEventButton *event) {
|
2015-03-19 01:02:11 +08:00
|
|
|
return _widget.event((GdkEvent*) event);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Gtk::VScrollbar _scrollbar;
|
|
|
|
TextWidget _widget;
|
|
|
|
EditorOverlay _overlay;
|
|
|
|
Gtk::HBox _box;
|
|
|
|
};
|
|
|
|
|
2016-05-10 07:13:49 +08:00
|
|
|
std::unique_ptr<TextWindowGtk> TW;
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
void ShowTextWindow(bool visible) {
|
|
|
|
if(visible)
|
|
|
|
TW->show();
|
|
|
|
else
|
|
|
|
TW->hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
void GetTextWindowSize(int *w, int *h) {
|
|
|
|
Gdk::Rectangle allocation = TW->get_widget().get_allocation();
|
|
|
|
*w = allocation.get_width();
|
|
|
|
*h = allocation.get_height();
|
|
|
|
}
|
|
|
|
|
|
|
|
void InvalidateText(void) {
|
|
|
|
TW->get_widget().queue_draw();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MoveTextScrollbarTo(int pos, int maxPos, int page) {
|
|
|
|
TW->get_scrollbar().get_adjustment()->configure(pos, 0, maxPos, 1, 10, page);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetMousePointerToHand(bool is_hand) {
|
|
|
|
TW->get_widget().set_cursor_hand(is_hand);
|
|
|
|
}
|
|
|
|
|
2015-11-06 16:40:12 +08:00
|
|
|
void ShowTextEditControl(int x, int y, const std::string &val) {
|
2016-04-16 08:10:32 +08:00
|
|
|
TW->get_overlay().start_editing(x, y, TextWindow::CHAR_HEIGHT, /*is_monospace=*/true, 30, val);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void HideTextEditControl(void) {
|
|
|
|
TW->get_overlay().stop_editing();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TextEditControlIsVisible(void) {
|
|
|
|
return TW->get_overlay().is_editing();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Miscellanea */
|
|
|
|
|
|
|
|
|
|
|
|
void DoMessageBox(const char *message, int rows, int cols, bool error) {
|
|
|
|
Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true,
|
|
|
|
error ? Gtk::MESSAGE_ERROR : Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK,
|
|
|
|
/*is_modal*/ true);
|
|
|
|
dialog.set_title(error ? "SolveSpace - Error" : "SolveSpace - Message");
|
|
|
|
dialog.run();
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenWebsite(const char *url) {
|
|
|
|
gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* fontconfig is already initialized by GTK */
|
Rewrite TTF to Bezier conversion using Freetype.
Benefits:
* Much simpler code.
* Handles the entire TTF spec, not just a small subset that
only really worked well on Windows fonts.
* Handles all character sets as well as accented characters.
* Much faster parsing, since Freetype lazily loads and
caches glyphs.
* Support for basically every kind of font that was invented,
not just TTF.
Note that OpenType features, e.g. ligatures, are not yet supported.
This means that Arabic and Devanagari scripts, among others, will
not be rendered in their proper form.
RTL scripts are not supported either, neither in TTF nor in
the text window. Adding RTL support is comparatively easy, but
given that Arabic would not be legibly rendered anyway, this is not
done so far.
2016-01-30 09:42:44 +08:00
|
|
|
std::vector<std::string> GetFontFiles() {
|
|
|
|
std::vector<std::string> fonts;
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
FcPattern *pat = FcPatternCreate();
|
|
|
|
FcObjectSet *os = FcObjectSetBuild(FC_FILE, (char *)0);
|
|
|
|
FcFontSet *fs = FcFontList(0, pat, os);
|
|
|
|
|
|
|
|
for(int i = 0; i < fs->nfont; i++) {
|
2015-12-27 09:03:24 +08:00
|
|
|
FcChar8 *filenameFC = FcPatternFormat(fs->fonts[i], (const FcChar8*) "%{file}");
|
|
|
|
std::string filename = (char*) filenameFC;
|
Rewrite TTF to Bezier conversion using Freetype.
Benefits:
* Much simpler code.
* Handles the entire TTF spec, not just a small subset that
only really worked well on Windows fonts.
* Handles all character sets as well as accented characters.
* Much faster parsing, since Freetype lazily loads and
caches glyphs.
* Support for basically every kind of font that was invented,
not just TTF.
Note that OpenType features, e.g. ligatures, are not yet supported.
This means that Arabic and Devanagari scripts, among others, will
not be rendered in their proper form.
RTL scripts are not supported either, neither in TTF nor in
the text window. Adding RTL support is comparatively easy, but
given that Arabic would not be legibly rendered anyway, this is not
done so far.
2016-01-30 09:42:44 +08:00
|
|
|
fonts.push_back(filename);
|
2015-12-27 09:03:24 +08:00
|
|
|
FcStrFree(filenameFC);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
FcFontSetDestroy(fs);
|
|
|
|
FcObjectSetDestroy(os);
|
|
|
|
FcPatternDestroy(pat);
|
Rewrite TTF to Bezier conversion using Freetype.
Benefits:
* Much simpler code.
* Handles the entire TTF spec, not just a small subset that
only really worked well on Windows fonts.
* Handles all character sets as well as accented characters.
* Much faster parsing, since Freetype lazily loads and
caches glyphs.
* Support for basically every kind of font that was invented,
not just TTF.
Note that OpenType features, e.g. ligatures, are not yet supported.
This means that Arabic and Devanagari scripts, among others, will
not be rendered in their proper form.
RTL scripts are not supported either, neither in TTF nor in
the text window. Adding RTL support is comparatively easy, but
given that Arabic would not be legibly rendered anyway, this is not
done so far.
2016-01-30 09:42:44 +08:00
|
|
|
|
|
|
|
return fonts;
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
Implement a resource system.
Currently, icons, fonts, etc are converted to C structures at compile
time and are hardcoded to the binary. This presents several problems:
* Cross-compilation is complicated. Right now, it is necessary
to be able to run executables for the target platform; this
happens to work with wine-binfmt installed, but is rather ugly.
* Icons can only have one resolution. On OS X, modern software is
expected to take advantage of high-DPI ("Retina") screens and
use so-called @2x assets when ran in high-DPI mode.
* Localization is complicated. Win32 and OS X provide built-in
support for loading the resource appropriate for the user's
locale.
* Embedding strings can only be done as raw strings, using C++'s
R"(...)" literals. This precludes embedding sizable strings,
e.g. JavaScript libraries as used in Three.js export, and makes
git history less useful. Not embedding the libraries means we
have to rely on external CDNs, which requires an Internet
connection and adds a glaring point of failure.
* Linux distribution guidelines are violated. All architecture-
independent data, especially large data such as fonts, is
expected to be in /usr/share, not in the binary.
* Customization is impossible without recompilation. Minor
modifications like adding a few missing vector font characters
or adjusting localization require a complete development
environment, which is unreasonable to expect from users of
a mechanical CAD.
As such, this commit adds a resource system that bundles (and
sometimes builds) resources with the executable. Where they go is
platform-dependent:
* on Win32: into resources of the executable, which allows us to
keep distributing one file;
* on OS X: into the app bundle;
* on other *nix: into /usr/share/solvespace/ or ../res/ (relative
to the executable path), the latter allowing us to run freshly
built executables without installation.
It also subsides the platform-specific resources that are in src/.
The resource system is not yet used for anything; this will be added
in later commits.
2016-04-21 23:54:18 +08:00
|
|
|
static std::string resource_dir;
|
|
|
|
const void *LoadResource(const std::string &name, size_t *size) {
|
|
|
|
static std::map<std::string, std::vector<uint8_t>> cache;
|
|
|
|
|
|
|
|
auto it = cache.find(name);
|
|
|
|
if(it == cache.end()) {
|
|
|
|
struct stat st;
|
|
|
|
std::string path;
|
|
|
|
|
|
|
|
path = (UNIX_DATADIR "/") + name;
|
|
|
|
if(stat(path.c_str(), &st)) {
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(errno == ENOENT, "Unexpected stat() error");
|
Implement a resource system.
Currently, icons, fonts, etc are converted to C structures at compile
time and are hardcoded to the binary. This presents several problems:
* Cross-compilation is complicated. Right now, it is necessary
to be able to run executables for the target platform; this
happens to work with wine-binfmt installed, but is rather ugly.
* Icons can only have one resolution. On OS X, modern software is
expected to take advantage of high-DPI ("Retina") screens and
use so-called @2x assets when ran in high-DPI mode.
* Localization is complicated. Win32 and OS X provide built-in
support for loading the resource appropriate for the user's
locale.
* Embedding strings can only be done as raw strings, using C++'s
R"(...)" literals. This precludes embedding sizable strings,
e.g. JavaScript libraries as used in Three.js export, and makes
git history less useful. Not embedding the libraries means we
have to rely on external CDNs, which requires an Internet
connection and adds a glaring point of failure.
* Linux distribution guidelines are violated. All architecture-
independent data, especially large data such as fonts, is
expected to be in /usr/share, not in the binary.
* Customization is impossible without recompilation. Minor
modifications like adding a few missing vector font characters
or adjusting localization require a complete development
environment, which is unreasonable to expect from users of
a mechanical CAD.
As such, this commit adds a resource system that bundles (and
sometimes builds) resources with the executable. Where they go is
platform-dependent:
* on Win32: into resources of the executable, which allows us to
keep distributing one file;
* on OS X: into the app bundle;
* on other *nix: into /usr/share/solvespace/ or ../res/ (relative
to the executable path), the latter allowing us to run freshly
built executables without installation.
It also subsides the platform-specific resources that are in src/.
The resource system is not yet used for anything; this will be added
in later commits.
2016-04-21 23:54:18 +08:00
|
|
|
path = resource_dir + "/" + name;
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(!stat(path.c_str(), &st), "Cannot find resource");
|
Implement a resource system.
Currently, icons, fonts, etc are converted to C structures at compile
time and are hardcoded to the binary. This presents several problems:
* Cross-compilation is complicated. Right now, it is necessary
to be able to run executables for the target platform; this
happens to work with wine-binfmt installed, but is rather ugly.
* Icons can only have one resolution. On OS X, modern software is
expected to take advantage of high-DPI ("Retina") screens and
use so-called @2x assets when ran in high-DPI mode.
* Localization is complicated. Win32 and OS X provide built-in
support for loading the resource appropriate for the user's
locale.
* Embedding strings can only be done as raw strings, using C++'s
R"(...)" literals. This precludes embedding sizable strings,
e.g. JavaScript libraries as used in Three.js export, and makes
git history less useful. Not embedding the libraries means we
have to rely on external CDNs, which requires an Internet
connection and adds a glaring point of failure.
* Linux distribution guidelines are violated. All architecture-
independent data, especially large data such as fonts, is
expected to be in /usr/share, not in the binary.
* Customization is impossible without recompilation. Minor
modifications like adding a few missing vector font characters
or adjusting localization require a complete development
environment, which is unreasonable to expect from users of
a mechanical CAD.
As such, this commit adds a resource system that bundles (and
sometimes builds) resources with the executable. Where they go is
platform-dependent:
* on Win32: into resources of the executable, which allows us to
keep distributing one file;
* on OS X: into the app bundle;
* on other *nix: into /usr/share/solvespace/ or ../res/ (relative
to the executable path), the latter allowing us to run freshly
built executables without installation.
It also subsides the platform-specific resources that are in src/.
The resource system is not yet used for anything; this will be added
in later commits.
2016-04-21 23:54:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<uint8_t> data(st.st_size);
|
|
|
|
FILE *f = ssfopen(path.c_str(), "rb");
|
2016-05-19 06:51:36 +08:00
|
|
|
ssassert(f != NULL, "Cannot open resource");
|
Implement a resource system.
Currently, icons, fonts, etc are converted to C structures at compile
time and are hardcoded to the binary. This presents several problems:
* Cross-compilation is complicated. Right now, it is necessary
to be able to run executables for the target platform; this
happens to work with wine-binfmt installed, but is rather ugly.
* Icons can only have one resolution. On OS X, modern software is
expected to take advantage of high-DPI ("Retina") screens and
use so-called @2x assets when ran in high-DPI mode.
* Localization is complicated. Win32 and OS X provide built-in
support for loading the resource appropriate for the user's
locale.
* Embedding strings can only be done as raw strings, using C++'s
R"(...)" literals. This precludes embedding sizable strings,
e.g. JavaScript libraries as used in Three.js export, and makes
git history less useful. Not embedding the libraries means we
have to rely on external CDNs, which requires an Internet
connection and adds a glaring point of failure.
* Linux distribution guidelines are violated. All architecture-
independent data, especially large data such as fonts, is
expected to be in /usr/share, not in the binary.
* Customization is impossible without recompilation. Minor
modifications like adding a few missing vector font characters
or adjusting localization require a complete development
environment, which is unreasonable to expect from users of
a mechanical CAD.
As such, this commit adds a resource system that bundles (and
sometimes builds) resources with the executable. Where they go is
platform-dependent:
* on Win32: into resources of the executable, which allows us to
keep distributing one file;
* on OS X: into the app bundle;
* on other *nix: into /usr/share/solvespace/ or ../res/ (relative
to the executable path), the latter allowing us to run freshly
built executables without installation.
It also subsides the platform-specific resources that are in src/.
The resource system is not yet used for anything; this will be added
in later commits.
2016-04-21 23:54:18 +08:00
|
|
|
fread(&data[0], 1, st.st_size, f);
|
|
|
|
fclose(f);
|
|
|
|
|
|
|
|
cache.emplace(name, std::move(data));
|
|
|
|
it = cache.find(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
*size = (*it).second.size();
|
|
|
|
return &(*it).second[0];
|
|
|
|
}
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
/* Space Navigator support */
|
|
|
|
|
|
|
|
#ifdef HAVE_SPACEWARE
|
2016-05-08 07:34:21 +08:00
|
|
|
static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *, gpointer) {
|
2015-03-19 01:02:11 +08:00
|
|
|
XEvent *xevent = (XEvent*) gxevent;
|
|
|
|
|
|
|
|
spnav_event sev;
|
|
|
|
if(!spnav_x11_event(xevent, &sev))
|
|
|
|
return GDK_FILTER_CONTINUE;
|
|
|
|
|
|
|
|
switch(sev.type) {
|
|
|
|
case SPNAV_EVENT_MOTION:
|
|
|
|
SS.GW.SpaceNavigatorMoved(
|
|
|
|
(double)sev.motion.x,
|
|
|
|
(double)sev.motion.y,
|
|
|
|
(double)sev.motion.z * -1.0,
|
|
|
|
(double)sev.motion.rx * 0.001,
|
|
|
|
(double)sev.motion.ry * 0.001,
|
|
|
|
(double)sev.motion.rz * -0.001,
|
|
|
|
xevent->xmotion.state & ShiftMask);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SPNAV_EVENT_BUTTON:
|
|
|
|
if(!sev.button.press && sev.button.bnum == SI_APP_FIT_BUTTON) {
|
|
|
|
SS.GW.SpaceNavigatorButtonUp();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return GDK_FILTER_REMOVE;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* Application lifecycle */
|
|
|
|
|
|
|
|
void ExitNow(void) {
|
|
|
|
GW->hide();
|
|
|
|
TW->hide();
|
|
|
|
}
|
2015-03-24 01:49:04 +08:00
|
|
|
};
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
int main(int argc, char** argv) {
|
2015-12-27 15:51:28 +08:00
|
|
|
/* It would in principle be possible to judiciously use
|
|
|
|
Glib::filename_{from,to}_utf8, but it's not really worth
|
|
|
|
the effort.
|
|
|
|
The setlocale() call is necessary for Glib::get_charset()
|
|
|
|
to detect the system character set; otherwise it thinks
|
|
|
|
it is always ANSI_X3.4-1968.
|
|
|
|
We set it back to C after all. */
|
|
|
|
setlocale(LC_ALL, "");
|
|
|
|
if(!Glib::get_charset()) {
|
|
|
|
std::cerr << "Sorry, only UTF-8 locales are supported." << std::endl;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
setlocale(LC_ALL, "C");
|
|
|
|
|
|
|
|
/* If we don't do this, gtk_init will set the C standard library
|
2015-03-19 01:02:11 +08:00
|
|
|
locale, and printf will format floats using ",". We will then
|
|
|
|
fail to parse these. Also, many text window lines will become
|
|
|
|
ambiguous. */
|
|
|
|
gtk_disable_setlocale();
|
|
|
|
|
Implement a resource system.
Currently, icons, fonts, etc are converted to C structures at compile
time and are hardcoded to the binary. This presents several problems:
* Cross-compilation is complicated. Right now, it is necessary
to be able to run executables for the target platform; this
happens to work with wine-binfmt installed, but is rather ugly.
* Icons can only have one resolution. On OS X, modern software is
expected to take advantage of high-DPI ("Retina") screens and
use so-called @2x assets when ran in high-DPI mode.
* Localization is complicated. Win32 and OS X provide built-in
support for loading the resource appropriate for the user's
locale.
* Embedding strings can only be done as raw strings, using C++'s
R"(...)" literals. This precludes embedding sizable strings,
e.g. JavaScript libraries as used in Three.js export, and makes
git history less useful. Not embedding the libraries means we
have to rely on external CDNs, which requires an Internet
connection and adds a glaring point of failure.
* Linux distribution guidelines are violated. All architecture-
independent data, especially large data such as fonts, is
expected to be in /usr/share, not in the binary.
* Customization is impossible without recompilation. Minor
modifications like adding a few missing vector font characters
or adjusting localization require a complete development
environment, which is unreasonable to expect from users of
a mechanical CAD.
As such, this commit adds a resource system that bundles (and
sometimes builds) resources with the executable. Where they go is
platform-dependent:
* on Win32: into resources of the executable, which allows us to
keep distributing one file;
* on OS X: into the app bundle;
* on other *nix: into /usr/share/solvespace/ or ../res/ (relative
to the executable path), the latter allowing us to run freshly
built executables without installation.
It also subsides the platform-specific resources that are in src/.
The resource system is not yet used for anything; this will be added
in later commits.
2016-04-21 23:54:18 +08:00
|
|
|
resource_dir = argv[0]; // .../src/solvespace
|
|
|
|
resource_dir.erase(resource_dir.rfind('/'));
|
|
|
|
resource_dir.erase(resource_dir.rfind('/'));
|
|
|
|
resource_dir += "/res"; // .../res
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
Gtk::Main main(argc, argv);
|
|
|
|
|
|
|
|
#ifdef HAVE_SPACEWARE
|
|
|
|
gdk_window_add_filter(NULL, GdkSpnavFilter, NULL);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
CnfLoad();
|
|
|
|
|
2016-05-19 01:23:14 +08:00
|
|
|
SolveSpace::Pixmap icon = LoadPNG("freedesktop/solvespace-48x48.png");
|
|
|
|
Glib::RefPtr<Gdk::Pixbuf> icon_gdk =
|
|
|
|
Gdk::Pixbuf::create_from_data(&icon.data[0], Gdk::COLORSPACE_RGB,
|
|
|
|
icon.hasAlpha, 8, icon.width, icon.height, icon.stride);
|
|
|
|
|
2016-05-10 07:13:49 +08:00
|
|
|
TW.reset(new TextWindowGtk);
|
|
|
|
GW.reset(new GraphicsWindowGtk);
|
2015-03-19 01:02:11 +08:00
|
|
|
InitMainMenu(&GW->get_menubar());
|
|
|
|
GW->get_menubar().accelerate(*TW);
|
2016-05-10 07:13:49 +08:00
|
|
|
TW->set_transient_for(*GW);
|
2016-05-19 01:23:14 +08:00
|
|
|
GW->set_icon(icon_gdk);
|
|
|
|
TW->set_icon(icon_gdk);
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
TW->show_all();
|
|
|
|
GW->show_all();
|
|
|
|
|
2015-03-24 14:45:53 +08:00
|
|
|
SS.Init();
|
|
|
|
|
2015-03-19 01:02:11 +08:00
|
|
|
if(argc >= 2) {
|
|
|
|
if(argc > 2) {
|
|
|
|
std::cerr << "Only the first file passed on command line will be opened."
|
|
|
|
<< std::endl;
|
|
|
|
}
|
|
|
|
|
2015-12-27 15:51:28 +08:00
|
|
|
/* Make sure the argument is valid UTF-8. */
|
|
|
|
SS.OpenFile(Glib::ustring(argv[1]));
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
main.run(*GW);
|
|
|
|
|
2016-05-10 07:13:49 +08:00
|
|
|
TW.reset();
|
|
|
|
GW.reset();
|
2015-03-19 01:02:11 +08:00
|
|
|
|
|
|
|
SK.Clear();
|
|
|
|
SS.Clear();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|