2015-03-19 01:02:11 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Our main() function, and GTK3-specific stuff to set up our windows and
|
|
|
|
// otherwise handle our interface to the operating system. Everything
|
|
|
|
// outside gtk/... should be standard C++ and OpenGL.
|
|
|
|
//
|
|
|
|
// Copyright 2015 <whitequark@whitequark.org>
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include <errno.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <time.h>
|
|
|
|
|
|
|
|
#include <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"
|
2015-03-21 03:35:04 +08:00
|
|
|
#include "../unix/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;
|
|
|
|
if(!glXQueryVersion(_xdisplay, &glxmajor, &glxminor)) {
|
|
|
|
dbp("OpenGL is not supported");
|
|
|
|
oops();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(glxmajor < 1 || (glxmajor == 1 && glxminor < 3)) {
|
|
|
|
dbp("GLX version %d.%d is too old; 1.3 required", glxmajor, glxminor);
|
|
|
|
oops();
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
if(!fbconfigs || fbconfig_num == 0)
|
|
|
|
oops();
|
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);
|
|
|
|
if(!_glcontext) {
|
|
|
|
dbp("cannot create OpenGL context");
|
2015-03-19 01:02:11 +08:00
|
|
|
oops();
|
|
|
|
}
|
|
|
|
|
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. */
|
|
|
|
virtual bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
|
2015-03-21 03:35:04 +08:00
|
|
|
if(!glXMakeCurrent(_xdisplay, _xwindow, _glcontext))
|
2015-03-19 01:02:11 +08:00
|
|
|
oops();
|
|
|
|
|
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();
|
2015-03-21 03:35:04 +08:00
|
|
|
if(!_offscreen->begin(allocation.get_width(), allocation.get_height()))
|
|
|
|
oops();
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef HAVE_GTK2
|
2016-05-08 07:34:21 +08:00
|
|
|
virtual bool on_expose_event(GdkEventExpose *) {
|
2015-03-19 01:02:11 +08:00
|
|
|
return on_draw(get_window()->create_cairo_context());
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
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) {
|
|
|
|
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:
|
|
|
|
virtual bool on_key_press_event(GdkEventKey *event) {
|
|
|
|
if(event->keyval == GDK_KEY_Escape) {
|
|
|
|
stop_editing();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_size_allocate(Gtk::Allocation& allocation) {
|
|
|
|
Gtk::Fixed::on_size_allocate(allocation);
|
|
|
|
|
|
|
|
_underlay.size_allocate(allocation);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_activate() {
|
|
|
|
_signal_editing_done(_entry.get_text());
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Gtk::Widget &_underlay;
|
|
|
|
Gtk::Entry _entry;
|
|
|
|
sigc::signal<void, Glib::ustring> _signal_editing_done;
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Graphics window */
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
void emulate_key_press(GdkEventKey *event) {
|
|
|
|
on_key_press_event(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
virtual bool on_configure_event(GdkEventConfigure *event) {
|
|
|
|
_w = event->width;
|
|
|
|
_h = event->height;
|
|
|
|
|
|
|
|
return GlWidget::on_configure_event(event);;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_gl_draw() {
|
|
|
|
SS.GW.Paint();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_motion_notify_event(GdkEventMotion *event) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_button_press_event(GdkEventButton *event) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_button_release_event(GdkEventButton *event) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_scroll_event(GdkEventScroll *event) {
|
|
|
|
int x, y;
|
|
|
|
ij_to_xy(event->x, event->y, x, y);
|
|
|
|
|
|
|
|
SS.GW.MouseScroll(x, y, -DeltaYOfScrollEvent(event));
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-08 07:34:21 +08:00
|
|
|
virtual bool on_leave_notify_event (GdkEventCrossing *) {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.GW.MouseLeave();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_key_press_event(GdkEventKey *event) {
|
|
|
|
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;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
int _w, _h;
|
|
|
|
void ij_to_xy(int i, int j, int &x, int &y) {
|
|
|
|
// Convert to xy (vs. ij) style coordinates,
|
|
|
|
// with (0, 0) at center
|
|
|
|
x = i - _w / 2;
|
|
|
|
y = _h / 2 - j;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
virtual void on_show() {
|
|
|
|
Gtk::Window::on_show();
|
|
|
|
|
|
|
|
CnfThawWindowPos(this, "GraphicsWindow");
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_hide() {
|
|
|
|
CnfFreezeWindowPos(this, "GraphicsWindow");
|
|
|
|
|
|
|
|
Gtk::Window::on_hide();
|
|
|
|
}
|
|
|
|
|
2016-05-08 07:34:21 +08:00
|
|
|
virtual bool on_delete_event(GdkEventAny *) {
|
2015-03-19 01:02:11 +08:00
|
|
|
SS.Exit();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_window_state_event(GdkEventWindowState *event) {
|
|
|
|
_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);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_editing_done(Glib::ustring value) {
|
|
|
|
SS.GW.EditControlDone(value.c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
GraphicsWidget _widget;
|
|
|
|
EditorOverlay _overlay;
|
|
|
|
Gtk::MenuBar _menubar;
|
|
|
|
Gtk::VBox _box;
|
|
|
|
|
|
|
|
bool _is_fullscreen;
|
|
|
|
};
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
GraphicsWindowGtk *GW = NULL;
|
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:
|
|
|
|
virtual void on_activate() {
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
virtual bool on_button_press_event(GdkEventButton *event) {
|
|
|
|
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) {
|
|
|
|
if(context_submenu) oops();
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
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:
|
|
|
|
virtual void on_activate() {
|
|
|
|
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-08 07:34:21 +08:00
|
|
|
if((unsigned)entry->level >= sizeof(levels) / sizeof(levels[0]))
|
2015-03-19 01:02:11 +08:00
|
|
|
oops();
|
|
|
|
|
|
|
|
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:
|
|
|
|
virtual void on_activate() {
|
|
|
|
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-04 11:12:06 +08:00
|
|
|
chooser->add_filter(*filter);
|
|
|
|
if(is_active)
|
|
|
|
chooser->set_filter(*filter);
|
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);
|
|
|
|
if(lparen < 0 || rdelim < 0)
|
|
|
|
oops();
|
|
|
|
|
|
|
|
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:
|
|
|
|
virtual void on_gl_draw() {
|
|
|
|
SS.TW.Paint();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_motion_notify_event(GdkEventMotion *event) {
|
|
|
|
SS.TW.MouseEvent(/*leftClick*/ false,
|
|
|
|
/*leftDown*/ event->state & GDK_BUTTON1_MASK,
|
|
|
|
event->x, event->y);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_button_press_event(GdkEventButton *event) {
|
|
|
|
SS.TW.MouseEvent(/*leftClick*/ event->type == GDK_BUTTON_PRESS,
|
|
|
|
/*leftDown*/ event->state & GDK_BUTTON1_MASK,
|
|
|
|
event->x, event->y);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_scroll_event(GdkEventScroll *event) {
|
|
|
|
_adjustment->set_value(_adjustment->get_value() +
|
|
|
|
DeltaYOfScrollEvent(event) * _adjustment->get_page_increment());
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-05-08 07:34:21 +08:00
|
|
|
virtual bool on_leave_notify_event (GdkEventCrossing *) {
|
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_keep_above(true);
|
|
|
|
set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY);
|
|
|
|
set_skip_taskbar_hint(true);
|
|
|
|
set_skip_pager_hint(true);
|
|
|
|
set_title("SolveSpace - Browser");
|
|
|
|
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:
|
|
|
|
virtual void on_show() {
|
|
|
|
Gtk::Window::on_show();
|
|
|
|
|
|
|
|
CnfThawWindowPos(this, "TextWindow");
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_hide() {
|
|
|
|
CnfFreezeWindowPos(this, "TextWindow");
|
|
|
|
|
|
|
|
Gtk::Window::on_hide();
|
|
|
|
}
|
|
|
|
|
2016-05-08 07:34:21 +08:00
|
|
|
virtual bool on_delete_event(GdkEventAny *) {
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_scrollbar_value_changed() {
|
|
|
|
SS.TW.ScrollbarEvent(_scrollbar.get_adjustment()->get_value());
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void on_editing_done(Glib::ustring value) {
|
|
|
|
SS.TW.EditControlDone(value.c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_editor_motion_notify_event(GdkEventMotion *event) {
|
|
|
|
return _widget.event((GdkEvent*) event);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_editor_button_press_event(GdkEventButton *event) {
|
|
|
|
return _widget.event((GdkEvent*) event);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Gtk::VScrollbar _scrollbar;
|
|
|
|
TextWidget _widget;
|
|
|
|
EditorOverlay _overlay;
|
|
|
|
Gtk::HBox _box;
|
|
|
|
};
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
TextWindowGtk *TW = NULL;
|
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
|
|
|
}
|
|
|
|
|
|
|
|
/* 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();
|
|
|
|
|
|
|
|
Gtk::Main main(argc, argv);
|
|
|
|
|
|
|
|
#ifdef HAVE_SPACEWARE
|
|
|
|
gdk_window_add_filter(NULL, GdkSpnavFilter, NULL);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
CnfLoad();
|
|
|
|
|
2015-03-24 01:49:04 +08:00
|
|
|
TW = new TextWindowGtk;
|
|
|
|
GW = new GraphicsWindowGtk;
|
2015-03-19 01:02:11 +08:00
|
|
|
InitMainMenu(&GW->get_menubar());
|
|
|
|
GW->get_menubar().accelerate(*TW);
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
delete GW;
|
|
|
|
delete TW;
|
|
|
|
|
|
|
|
SK.Clear();
|
|
|
|
SS.Clear();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|