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>
|
|
|
|
#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>
|
|
|
|
|
|
|
|
#undef HAVE_STDINT_H /* no thanks, we have our own config.h */
|
|
|
|
|
|
|
|
#include <GL/glx.h>
|
|
|
|
|
|
|
|
#include <config.h>
|
2015-03-21 03:35:04 +08:00
|
|
|
#include "solvespace.h"
|
|
|
|
#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
|
|
|
char RecentFile[MAX_RECENT][MAX_PATH];
|
|
|
|
|
|
|
|
#define GL_CHECK() \
|
|
|
|
do { \
|
|
|
|
int err = (int)glGetError(); \
|
2015-03-21 03:35:04 +08:00
|
|
|
if(err) dbp("%s:%d: glGetError() == 0x%X %s", \
|
|
|
|
__FILE__, __LINE__, err, gluErrorString(err)); \
|
2015-03-19 01:02:11 +08:00
|
|
|
} while (0)
|
|
|
|
|
|
|
|
/* 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;
|
|
|
|
|
|
|
|
static int CnfPrepare(char *path, int pathsz) {
|
|
|
|
// Refer to http://standards.freedesktop.org/basedir-spec/latest/
|
|
|
|
|
|
|
|
const char *xdg_home, *home;
|
|
|
|
xdg_home = getenv("XDG_CONFIG_HOME");
|
|
|
|
home = getenv("HOME");
|
|
|
|
|
|
|
|
char dir[MAX_PATH];
|
|
|
|
int dirlen;
|
|
|
|
if(xdg_home)
|
|
|
|
dirlen = snprintf(dir, sizeof(dir), "%s/solvespace", xdg_home);
|
|
|
|
else if(home)
|
|
|
|
dirlen = snprintf(dir, sizeof(dir), "%s/.config/solvespace", home);
|
|
|
|
else {
|
|
|
|
dbp("neither XDG_CONFIG_HOME nor HOME is set");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(dirlen >= sizeof(dir))
|
|
|
|
oops();
|
|
|
|
|
|
|
|
struct stat st;
|
|
|
|
if(stat(dir, &st)) {
|
|
|
|
if(errno == ENOENT) {
|
|
|
|
if(mkdir(dir, 0777)) {
|
|
|
|
dbp("cannot mkdir %s: %s", dir, strerror(errno));
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dbp("cannot stat %s: %s", dir, strerror(errno));
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
} else if(!S_ISDIR(st.st_mode)) {
|
|
|
|
dbp("%s is not a directory", dir);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int pathlen = snprintf(path, pathsz, "%s/settings.json", dir);
|
|
|
|
if(pathlen >= pathsz)
|
|
|
|
oops();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CnfLoad() {
|
|
|
|
char path[MAX_PATH];
|
|
|
|
if(CnfPrepare(path, sizeof(path)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(settings)
|
|
|
|
json_object_put(settings); // deallocate
|
|
|
|
|
|
|
|
settings = json_object_from_file(path);
|
|
|
|
if(!settings) {
|
|
|
|
if(errno != ENOENT)
|
|
|
|
dbp("cannot load settings: %s", strerror(errno));
|
|
|
|
|
|
|
|
settings = json_object_new_object();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CnfSave() {
|
|
|
|
char path[MAX_PATH];
|
|
|
|
if(CnfPrepare(path, sizeof(path)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(json_object_to_file_ext(path, settings, JSON_C_TO_STRING_PRETTY))
|
|
|
|
dbp("cannot save settings: %s", strerror(errno));
|
|
|
|
}
|
|
|
|
|
|
|
|
void CnfFreezeInt(uint32_t val, const char *key) {
|
|
|
|
struct json_object *jval = json_object_new_int(val);
|
|
|
|
json_object_object_add(settings, key, jval);
|
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t CnfThawInt(uint32_t val, const char *key) {
|
|
|
|
struct json_object *jval;
|
|
|
|
if(json_object_object_get_ex(settings, key, &jval))
|
|
|
|
return json_object_get_int(jval);
|
|
|
|
else return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
void CnfFreezeFloat(float val, const char *key) {
|
|
|
|
struct json_object *jval = json_object_new_double(val);
|
|
|
|
json_object_object_add(settings, key, jval);
|
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
|
|
|
float CnfThawFloat(float val, const char *key) {
|
|
|
|
struct json_object *jval;
|
|
|
|
if(json_object_object_get_ex(settings, key, &jval))
|
|
|
|
return json_object_get_double(jval);
|
|
|
|
else return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
void CnfFreezeString(const char *val, const char *key) {
|
|
|
|
struct json_object *jval = json_object_new_string(val);
|
|
|
|
json_object_object_add(settings, key, jval);
|
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
|
|
|
void CnfThawString(char *val, int valsz, const char *key) {
|
|
|
|
struct json_object *jval;
|
|
|
|
if(json_object_object_get_ex(settings, key, &jval))
|
|
|
|
snprintf(val, valsz, "%s", json_object_get_string(jval));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CnfFreezeWindowPos(Gtk::Window *win, const char *key) {
|
|
|
|
int x, y, w, h;
|
|
|
|
win->get_position(x, y);
|
|
|
|
win->get_size(w, h);
|
|
|
|
|
|
|
|
char buf[100];
|
|
|
|
snprintf(buf, sizeof(buf), "%s_left", key);
|
|
|
|
CnfFreezeInt(x, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s_top", key);
|
|
|
|
CnfFreezeInt(y, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s_width", key);
|
|
|
|
CnfFreezeInt(w, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s_height", key);
|
|
|
|
CnfFreezeInt(h, buf);
|
|
|
|
|
|
|
|
CnfSave();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CnfThawWindowPos(Gtk::Window *win, const char *key) {
|
|
|
|
int x, y, w, h;
|
|
|
|
win->get_position(x, y);
|
|
|
|
win->get_size(w, h);
|
|
|
|
|
|
|
|
char buf[100];
|
|
|
|
snprintf(buf, sizeof(buf), "%s_left", key);
|
|
|
|
x = CnfThawInt(x, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s_top", key);
|
|
|
|
y = CnfThawInt(y, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s_width", key);
|
|
|
|
w = CnfThawInt(w, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s_height", key);
|
|
|
|
h = CnfThawInt(h, buf);
|
|
|
|
|
|
|
|
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 */
|
|
|
|
|
|
|
|
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
|
|
|
|
virtual bool on_expose_event(GdkEventExpose *event) {
|
|
|
|
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);
|
|
|
|
|
|
|
|
Pango::FontDescription desc;
|
|
|
|
desc.set_family("monospace");
|
|
|
|
desc.set_size(7000);
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
_entry.override_font(desc);
|
|
|
|
#else
|
|
|
|
_entry.modify_font(desc);
|
|
|
|
#endif
|
|
|
|
_entry.set_width_chars(30);
|
|
|
|
_entry.set_no_show_all(true);
|
|
|
|
add(_entry);
|
|
|
|
|
|
|
|
_entry.signal_activate().
|
|
|
|
connect(sigc::mem_fun(this, &EditorOverlay::on_activate));
|
|
|
|
}
|
|
|
|
|
|
|
|
void start_editing(int x, int y, const char *val) {
|
|
|
|
move(_entry, x, y - 4);
|
|
|
|
_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;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_leave_notify_event (GdkEventCrossing*event) {
|
|
|
|
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:
|
2015-03-24 01:49:04 +08:00
|
|
|
GraphicsWindowGtk() : _overlay(_widget) {
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_delete_event(GdkEventAny *event) {
|
|
|
|
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-03-24 14:45:53 +08:00
|
|
|
void SetCurrentFilename(const char *filename) {
|
|
|
|
if(filename) {
|
|
|
|
GW->set_title(std::string("SolveSpace - ") + filename);
|
|
|
|
} 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShowGraphicsEditControl(int x, int y, char *val) {
|
|
|
|
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;
|
|
|
|
j = -y + rect.get_height() / 2 - 24;
|
|
|
|
|
|
|
|
GW->get_overlay().start_editing(i, j, val);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
for(int i = 0; i < label.length(); i++) {
|
|
|
|
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);
|
|
|
|
|
|
|
|
if(entry->level >= sizeof(levels) / sizeof(levels[0]))
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ActivateMenuById(int id) {
|
|
|
|
main_menu_items[id]->activate();
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2015-03-19 01:02:11 +08:00
|
|
|
else if(_id >= RECENT_IMPORT && _id < (RECENT_IMPORT + MAX_RECENT))
|
|
|
|
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);
|
|
|
|
RefreshRecentMenu(GraphicsWindow::MNU_GROUP_RECENT, RECENT_IMPORT);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Save/load */
|
|
|
|
|
|
|
|
static void FiltersFromPattern(const char *active, const char *patterns,
|
|
|
|
Gtk::FileChooser &chooser) {
|
|
|
|
Glib::ustring uactive = "*." + Glib::ustring(active);
|
|
|
|
Glib::ustring upatterns = patterns;
|
|
|
|
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
Glib::RefPtr<Gtk::FileFilter> filter = Gtk::FileFilter::create();
|
|
|
|
#else
|
|
|
|
Gtk::FileFilter *filter = new Gtk::FileFilter;
|
|
|
|
#endif
|
|
|
|
Glib::ustring desc = "";
|
|
|
|
bool has_name = false, is_active = false;
|
|
|
|
int last = 0;
|
|
|
|
for(int i = 0; i <= upatterns.length(); i++) {
|
|
|
|
if(upatterns[i] == '\t' || upatterns[i] == '\n' || upatterns[i] == '\0') {
|
|
|
|
Glib::ustring frag = upatterns.substr(last, i - last);
|
|
|
|
if(!has_name) {
|
|
|
|
filter->set_name(frag);
|
|
|
|
has_name = true;
|
|
|
|
} else {
|
|
|
|
filter->add_pattern(frag);
|
|
|
|
if(uactive == frag)
|
|
|
|
is_active = true;
|
|
|
|
if(desc == "")
|
|
|
|
desc = frag;
|
|
|
|
else
|
|
|
|
desc += ", " + frag;
|
|
|
|
}
|
|
|
|
} else continue;
|
|
|
|
|
|
|
|
if(upatterns[i] == '\n' || upatterns[i] == '\0') {
|
|
|
|
filter->set_name(filter->get_name() + " (" + desc + ")");
|
|
|
|
#ifdef HAVE_GTK3
|
|
|
|
chooser.add_filter(filter);
|
|
|
|
if(is_active)
|
|
|
|
chooser.set_filter(filter);
|
|
|
|
|
|
|
|
filter = Gtk::FileFilter::create();
|
|
|
|
#else
|
|
|
|
chooser.add_filter(*filter);
|
|
|
|
if(is_active)
|
|
|
|
chooser.set_filter(*filter);
|
|
|
|
|
|
|
|
filter = new Gtk::FileFilter();
|
|
|
|
#endif
|
|
|
|
has_name = false;
|
|
|
|
is_active = false;
|
|
|
|
desc = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
last = i + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GetOpenFile(char *file, const char *active, const char *patterns) {
|
|
|
|
Gtk::FileChooserDialog chooser(*GW, "SolveSpace - Open File");
|
|
|
|
chooser.set_filename(file);
|
|
|
|
chooser.add_button("_Cancel", Gtk::RESPONSE_CANCEL);
|
|
|
|
chooser.add_button("_Open", Gtk::RESPONSE_OK);
|
|
|
|
|
|
|
|
char current_folder[MAX_PATH];
|
|
|
|
CnfThawString(current_folder, sizeof(current_folder), "FileChooserPath");
|
|
|
|
chooser.set_current_folder(current_folder);
|
|
|
|
|
|
|
|
FiltersFromPattern(active, patterns, chooser);
|
|
|
|
|
|
|
|
if(chooser.run() == Gtk::RESPONSE_OK) {
|
|
|
|
CnfFreezeString(chooser.get_current_folder().c_str(), "FileChooserPath");
|
|
|
|
strcpy(file, chooser.get_filename().c_str());
|
|
|
|
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();
|
|
|
|
int lparen = filter_name.find('(') + 1;
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GetSaveFile(char *file, const char *active, const char *patterns) {
|
|
|
|
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);
|
|
|
|
|
|
|
|
FiltersFromPattern(active, patterns, chooser);
|
|
|
|
|
|
|
|
char current_folder[MAX_PATH];
|
|
|
|
CnfThawString(current_folder, sizeof(current_folder), "FileChooserPath");
|
|
|
|
chooser.set_current_folder(current_folder);
|
|
|
|
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) {
|
|
|
|
CnfFreezeString(chooser.get_current_folder().c_str(), "FileChooserPath");
|
|
|
|
strcpy(file, chooser.get_filename().c_str());
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int SaveFileYesNoCancel(void) {
|
|
|
|
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:
|
|
|
|
return SAVE_YES;
|
|
|
|
|
|
|
|
case Gtk::RESPONSE_NO:
|
|
|
|
return SAVE_NO;
|
|
|
|
|
|
|
|
case Gtk::RESPONSE_CANCEL:
|
|
|
|
default:
|
|
|
|
return SAVE_CANCEL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-29 12:46:57 +08:00
|
|
|
int LoadAutosaveYesNo(void) {
|
|
|
|
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:
|
|
|
|
return SAVE_YES;
|
|
|
|
|
|
|
|
case Gtk::RESPONSE_NO:
|
|
|
|
default:
|
|
|
|
return SAVE_NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_leave_notify_event (GdkEventCrossing*event) {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool on_delete_event(GdkEventAny *event) {
|
|
|
|
/* 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShowTextEditControl(int x, int y, char *val) {
|
|
|
|
TW->get_overlay().start_editing(x, y, val);
|
|
|
|
}
|
|
|
|
|
|
|
|
void HideTextEditControl(void) {
|
|
|
|
TW->get_overlay().stop_editing();
|
|
|
|
GW->raise();
|
|
|
|
}
|
|
|
|
|
|
|
|
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 */
|
|
|
|
void LoadAllFontFiles(void) {
|
|
|
|
FcPattern *pat = FcPatternCreate();
|
|
|
|
FcObjectSet *os = FcObjectSetBuild(FC_FILE, (char *)0);
|
|
|
|
FcFontSet *fs = FcFontList(0, pat, os);
|
|
|
|
|
|
|
|
for(int i = 0; i < fs->nfont; i++) {
|
|
|
|
FcChar8 *filename = FcPatternFormat(fs->fonts[i], (const FcChar8*) "%{file}");
|
|
|
|
Glib::ustring ufilename = (char*) filename;
|
|
|
|
if(ufilename.length() > 4 &&
|
|
|
|
ufilename.substr(ufilename.length() - 4, 4).lowercase() == ".ttf") {
|
|
|
|
TtfFont tf;
|
|
|
|
ZERO(&tf);
|
|
|
|
strcpy(tf.fontFile, (char*) filename);
|
|
|
|
SS.fonts.l.Add(&tf);
|
|
|
|
}
|
|
|
|
FcStrFree(filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
FcFontSetDestroy(fs);
|
|
|
|
FcObjectSetDestroy(os);
|
|
|
|
FcPatternDestroy(pat);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Space Navigator support */
|
|
|
|
|
|
|
|
#ifdef HAVE_SPACEWARE
|
|
|
|
static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *event, gpointer data) {
|
|
|
|
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) {
|
|
|
|
/* If we don't call this, gtk_init will set the C standard library
|
|
|
|
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-03-24 14:45:53 +08:00
|
|
|
SS.OpenFile(argv[1]);
|
2015-03-19 01:02:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
main.run(*GW);
|
|
|
|
|
|
|
|
delete GW;
|
|
|
|
delete TW;
|
|
|
|
|
|
|
|
SK.Clear();
|
|
|
|
SS.Clear();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|