1280 lines
33 KiB
C++
1280 lines
33 KiB
C++
//-----------------------------------------------------------------------------
|
|
// Our main() function, and FLTK-specific stuff to set up our windows and
|
|
// otherwise handle our interface to the operating system. Everything
|
|
// outside fltk/... should be standard C++ and OpenGL.
|
|
//
|
|
// Copyright 2008-2013 Jonathan Westhues.
|
|
// Copyright 2013 Daniel Richard G. <skunk@iSKUNK.ORG>
|
|
//-----------------------------------------------------------------------------
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include <stdarg.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
#ifdef HAVE_FONTCONFIG_FONTCONFIG_H
|
|
# include <fontconfig/fontconfig.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_LIBSPNAV
|
|
# include <spnav.h>
|
|
# ifndef SI_APP_FIT_BUTTON
|
|
# define SI_APP_FIT_BUTTON 31
|
|
# endif
|
|
#endif
|
|
|
|
#include <fltk/xFl_Gl_Window_Group.H> //#include <FL/Fl_Gl_Window_Group.H>
|
|
#include <FL/Fl_Native_File_Chooser.H>
|
|
#include <FL/Fl_Preferences.H>
|
|
#include <FL/Fl_Sys_Menu_Bar.H>
|
|
#include <FL/forms.H> // for fl_gettime()
|
|
#include <FL/gl.h>
|
|
|
|
#include "solvespace.h"
|
|
|
|
#define fl_snprintf snprintf
|
|
|
|
static Fl_Preferences *Preferences = NULL;
|
|
|
|
class Graphics_Gl_Window;
|
|
class Text_Gl_Window;
|
|
|
|
static Fl_Window *GraphicsWnd = NULL;
|
|
static Graphics_Gl_Window *GraphicsGlWnd = NULL;
|
|
static Fl_Input *GraphicsEditControl = NULL;
|
|
static Fl_Sys_Menu_Bar *MenuBar = NULL;
|
|
static Fl_Menu_Item MenuBarItems[120];
|
|
static bool MenuBarVisible = true;
|
|
|
|
// Static window object to hold the non-fullscreen size of GraphicsWnd
|
|
static Fl_Window GraphicsWndOldSize(100, 100);
|
|
|
|
static Fl_Window *TextWnd = NULL;
|
|
static Text_Gl_Window *TextGlWnd = NULL;
|
|
static Fl_Scrollbar *TextWndScrollBar = NULL;
|
|
static Fl_Input *TextEditControl = NULL;
|
|
|
|
static struct {
|
|
int x, y;
|
|
} LastMousePos = { 0, 0 };
|
|
|
|
char RecentFile[MAX_RECENT][MAX_PATH];
|
|
static Fl_Menu_Item RecentOpenMenu[MAX_RECENT+1], RecentImportMenu[MAX_RECENT+1];
|
|
|
|
static Fl_Menu_Item ContextMenu[100];
|
|
static int ContextMenuCount = -1;
|
|
static Fl_Menu_Item ContextSubmenu[100];
|
|
static int ContextSubmenuCount = -1;
|
|
|
|
static long StartTimeSeconds = 0;
|
|
static const Fl_Font SS_FONT_MONOSPACE = FL_FREE_FONT + 8;
|
|
|
|
#define GL_CHECK() \
|
|
do { \
|
|
int err = (int)glGetError(); \
|
|
if(err) dbp("%s:%d: glGetError() == 0x%X\n", __FILE__, __LINE__, err); \
|
|
} while (0)
|
|
|
|
void DoMessageBox(const char *str, int rows, int cols, bool error)
|
|
{
|
|
fl_message_title(error ? "SolveSpace - Error" : "SolveSpace - Message");
|
|
if(error)
|
|
fl_alert("%s", str);
|
|
else
|
|
fl_message("%s", str);
|
|
}
|
|
|
|
void AddContextMenuItem(const char *label, int id)
|
|
{
|
|
if(ContextMenuCount < 0) {
|
|
ZERO(ContextMenu);
|
|
ContextMenuCount = 0;
|
|
}
|
|
|
|
// ContextMenu and ContextSubmenu are fixed-size arrays, because
|
|
// dynamic Fl_Menu_Item arrays are a PITA to work with
|
|
if(ContextMenuCount + 2 > (int)arraylen(ContextMenu)) oops();
|
|
if(ContextSubmenuCount > 0) {
|
|
if(ContextSubmenuCount + 2 > (int)arraylen(ContextSubmenu)) oops();
|
|
if(ContextMenuCount + ContextSubmenuCount + 3 > (int)arraylen(ContextMenu)) oops();
|
|
}
|
|
|
|
if(id == CONTEXT_SUBMENU) {
|
|
if(ContextSubmenuCount <= 0) oops();
|
|
|
|
Fl_Menu_Item *mi = ContextMenu + ContextMenuCount;
|
|
mi->label(label);
|
|
mi->flags = FL_SUBMENU;
|
|
ContextMenuCount++;
|
|
|
|
memcpy(ContextMenu + ContextMenuCount,
|
|
ContextSubmenu,
|
|
ContextSubmenuCount * sizeof(Fl_Menu_Item));
|
|
ContextMenuCount += ContextSubmenuCount + 1;
|
|
// (the +1 is for the null item that ends the submenu)
|
|
ContextSubmenuCount = -1;
|
|
} else {
|
|
Fl_Menu_Item *mi = ContextSubmenuCount >= 0 ?
|
|
ContextSubmenu + ContextSubmenuCount :
|
|
ContextMenu + ContextMenuCount;
|
|
|
|
int *cnt = ContextSubmenuCount >= 0 ?
|
|
&ContextSubmenuCount : &ContextMenuCount;
|
|
|
|
if(id == CONTEXT_SEPARATOR) {
|
|
if(*cnt < 1) oops();
|
|
(mi - 1)->flags |= FL_MENU_DIVIDER;
|
|
} else {
|
|
mi->label(label);
|
|
mi->argument(id);
|
|
++*cnt;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CreateContextSubmenu(void)
|
|
{
|
|
if(ContextSubmenuCount >= 0) oops();
|
|
ZERO(ContextSubmenu);
|
|
ContextSubmenuCount = 0;
|
|
}
|
|
|
|
int ShowContextMenu(void)
|
|
{
|
|
int r = 0;
|
|
if(ContextMenuCount > 0) {
|
|
const Fl_Menu_Item *mi =
|
|
ContextMenu->popup(Fl::event_x(), Fl::event_y());
|
|
if(mi) r = (int)mi->argument();
|
|
ContextMenuCount = -1;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static void TimerCallback(void *)
|
|
{
|
|
SS.GW.TimerCallback();
|
|
SS.TW.TimerCallback();
|
|
}
|
|
|
|
void SetTimerFor(int milliseconds)
|
|
{
|
|
Fl::add_timeout((double)milliseconds / 1000.0, TimerCallback);
|
|
}
|
|
|
|
void OpenWebsite(const char *url)
|
|
{
|
|
fl_open_uri(url, NULL, 0);
|
|
}
|
|
|
|
void ExitNow(void)
|
|
{
|
|
// This will make Fl::wait() return zero
|
|
GraphicsWnd->hide();
|
|
TextWnd->hide();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers so that we can read/write preference keys from the platform-
|
|
// independent code.
|
|
//-----------------------------------------------------------------------------
|
|
void CnfFreezeString(const char *str, const char *name)
|
|
{
|
|
if(Preferences) Preferences->set(name, str);
|
|
}
|
|
|
|
void CnfFreezeInt(uint32_t v, const char *name)
|
|
{
|
|
if(Preferences) Preferences->set(name, (int)v);
|
|
}
|
|
|
|
void CnfFreezeFloat(float v, const char *name)
|
|
{
|
|
if(Preferences) Preferences->set(name, v);
|
|
}
|
|
|
|
static void CnfFreezeWindowPos(Fl_Window *wnd, const char *name)
|
|
{
|
|
char buf[100];
|
|
fl_snprintf(buf, sizeof(buf), "%s_left", name);
|
|
CnfFreezeInt(wnd->x(), buf);
|
|
fl_snprintf(buf, sizeof(buf), "%s_top", name);
|
|
CnfFreezeInt(wnd->y(), buf);
|
|
fl_snprintf(buf, sizeof(buf), "%s_width", name);
|
|
CnfFreezeInt(wnd->w(), buf);
|
|
fl_snprintf(buf, sizeof(buf), "%s_height", name);
|
|
CnfFreezeInt(wnd->h(), buf);
|
|
}
|
|
|
|
void CnfThawString(char *str, int maxLen, const char *name)
|
|
{
|
|
char *def = strdup(str);
|
|
if(Preferences) Preferences->get(name, str, def, maxLen - 1);
|
|
free(def);
|
|
}
|
|
|
|
uint32_t CnfThawInt(uint32_t v, const char *name)
|
|
{
|
|
int r = 0;
|
|
if(Preferences) Preferences->get(name, r, (int)v);
|
|
return (uint32_t)r;
|
|
}
|
|
|
|
float CnfThawFloat(float v, const char *name)
|
|
{
|
|
float r = 0.0;
|
|
if(Preferences) Preferences->get(name, r, v);
|
|
return r;
|
|
}
|
|
|
|
static void CnfThawWindowPos(Fl_Window *wnd, const char *name)
|
|
{
|
|
char buf[100];
|
|
fl_snprintf(buf, sizeof(buf), "%s_left", name);
|
|
int x = CnfThawInt(wnd->x(), buf);
|
|
fl_snprintf(buf, sizeof(buf), "%s_top", name);
|
|
int y = CnfThawInt(wnd->y(), buf);
|
|
fl_snprintf(buf, sizeof(buf), "%s_width", name);
|
|
int w = CnfThawInt(wnd->w(), buf);
|
|
fl_snprintf(buf, sizeof(buf), "%s_height", name);
|
|
int h = CnfThawInt(wnd->h(), buf);
|
|
|
|
#define MARGIN 32
|
|
|
|
if(x < -MARGIN || y < -MARGIN) return;
|
|
if(x > Fl::w() - MARGIN || y > Fl::h() - MARGIN) return;
|
|
if(w < 100 || h < 100) return;
|
|
|
|
#undef MARGIN
|
|
|
|
wnd->resize(x, y, w, h);
|
|
}
|
|
|
|
static void LoadPreferences(void)
|
|
{
|
|
const char *xchome, *home;
|
|
char dir[MAX_PATH];
|
|
int r = 0;
|
|
|
|
// Refer to http://standards.freedesktop.org/basedir-spec/latest/
|
|
|
|
xchome = fl_getenv("XDG_CONFIG_HOME");
|
|
home = fl_getenv("HOME");
|
|
|
|
if(xchome)
|
|
r = fl_snprintf(dir, sizeof(dir), "%s/solvespace", xchome);
|
|
else if(home)
|
|
r = fl_snprintf(dir, sizeof(dir), "%s/.config/solvespace", home);
|
|
else
|
|
return;
|
|
|
|
if(r >= (int)sizeof(dir))
|
|
return;
|
|
|
|
if(!fl_filename_isdir(dir) && mkdir(dir, 0777) != 0) {
|
|
r = fl_snprintf(dir, sizeof(dir), "%s/.solvespace", home);
|
|
if(r >= (int)sizeof(dir))
|
|
return;
|
|
if(!fl_filename_isdir(dir))
|
|
if(mkdir(dir, 0777) != 0)
|
|
return;
|
|
}
|
|
|
|
Preferences = new Fl_Preferences(dir, "solvespace.org", "solvespace");
|
|
}
|
|
|
|
void SetWindowTitle(const char *str) {
|
|
GraphicsWnd->label(str);
|
|
}
|
|
|
|
void SetMousePointerToHand(bool yes) {
|
|
Fl_Cursor cur = yes ? FL_CURSOR_HAND : FL_CURSOR_ARROW;
|
|
GraphicsWnd->cursor(cur);
|
|
TextWnd->cursor(cur);
|
|
}
|
|
|
|
void MoveTextScrollbarTo(int pos, int maxPos, int page)
|
|
{
|
|
TextWndScrollBar->value(pos, page, 0, maxPos);
|
|
}
|
|
|
|
static void HandleTextWindowScrollBar(Fl_Widget *w)
|
|
{
|
|
if(w != TextWndScrollBar) oops();
|
|
SS.TW.ScrollbarEvent(TextWndScrollBar->value());
|
|
}
|
|
|
|
void ShowTextWindow(bool visible)
|
|
{
|
|
if(visible) {
|
|
TextWnd->show();
|
|
|
|
#ifdef InputHint
|
|
{
|
|
// Prevent the text window from gaining window manager focus by
|
|
// setting the appropriate WM hint via direct X calls
|
|
|
|
XWMHints *hints = XAllocWMHints();
|
|
hints->input = False;
|
|
hints->flags = InputHint;
|
|
XSetWMHints(fl_display, fl_xid(TextWnd), hints);
|
|
XFree(hints);
|
|
|
|
// In case the text window got the focus before we could set
|
|
// the hint, switch the focus back to the graphics window
|
|
|
|
Window xid = 0;
|
|
int revert_to = 0;
|
|
XGetInputFocus(fl_display, &xid, &revert_to);
|
|
if(xid == fl_xid(TextWnd)) {
|
|
XSetInputFocus(
|
|
fl_display,
|
|
fl_xid(GraphicsWnd),
|
|
RevertToParent,
|
|
CurrentTime);
|
|
}
|
|
}
|
|
#endif
|
|
} else {
|
|
TextWnd->hide();
|
|
}
|
|
}
|
|
|
|
int64_t GetMilliseconds(void)
|
|
{
|
|
long sec = StartTimeSeconds, usec = 0;
|
|
fl_gettime(&sec, &usec);
|
|
if(!StartTimeSeconds) StartTimeSeconds = sec;
|
|
sec -= StartTimeSeconds;
|
|
return 1000 * (int64_t)sec + (int64_t)usec / 1000;
|
|
}
|
|
|
|
int64_t GetUnixTime(void)
|
|
{
|
|
time_t ret;
|
|
time(&ret);
|
|
return (int64_t)ret;
|
|
}
|
|
|
|
void ShowTextEditControl(int x, int y, char *s)
|
|
{
|
|
if(GraphicsEditControlIsVisible()) return;
|
|
|
|
// Note: TextEditControl->position() does NOT set (x,y) position!
|
|
TextEditControl->resize(x, y, TextEditControl->w(), TextEditControl->h());
|
|
if(s) TextEditControl->value(s);
|
|
TextEditControl->show();
|
|
}
|
|
|
|
void HideTextEditControl(void)
|
|
{
|
|
TextEditControl->hide();
|
|
}
|
|
|
|
bool TextEditControlIsVisible(void)
|
|
{
|
|
return TextEditControl->visible();
|
|
}
|
|
|
|
void ShowGraphicsEditControl(int x, int y, char *s)
|
|
{
|
|
if(GraphicsEditControlIsVisible()) return;
|
|
|
|
GraphicsEditControl->position(x, y);
|
|
GraphicsEditControl->value(s);
|
|
GraphicsEditControl->show();
|
|
}
|
|
|
|
void HideGraphicsEditControl(void)
|
|
{
|
|
GraphicsEditControl->hide();
|
|
}
|
|
|
|
bool GraphicsEditControlIsVisible(void)
|
|
{
|
|
return GraphicsEditControl->visible();
|
|
}
|
|
|
|
class Graphics_Gl_Window : public Fl_Gl_Window_Group
|
|
{
|
|
public:
|
|
|
|
Graphics_Gl_Window(int x, int y, int w, int h)
|
|
: Fl_Gl_Window_Group(x, y, w, h)
|
|
{
|
|
mode(FL_RGB | FL_DOUBLE);
|
|
}
|
|
|
|
int handle_gl(int event)
|
|
{
|
|
switch(event)
|
|
{
|
|
#ifdef HAVE_LIBSPNAV
|
|
case FL_NO_EVENT: {
|
|
spnav_event sev;
|
|
if(!spnav_x11_event(fl_xevent, &sev)) break;
|
|
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,
|
|
Fl::event_shift());
|
|
break;
|
|
|
|
case SPNAV_EVENT_BUTTON:
|
|
if(!sev.button.press && sev.button.bnum == SI_APP_FIT_BUTTON) {
|
|
SS.GW.SpaceNavigatorButtonUp();
|
|
}
|
|
break;
|
|
}
|
|
return 1;
|
|
}
|
|
#endif // HAVE_LIBSPNAV
|
|
|
|
case FL_PUSH: // mouse button click...
|
|
case FL_RELEASE: // ...and release
|
|
case FL_DRAG:
|
|
case FL_MOVE: {
|
|
int x = Fl::event_x();
|
|
int y = Fl::event_y();
|
|
|
|
// Convert to xy (vs. ij) style coordinates,
|
|
// with (0, 0) at center
|
|
x = x - w() / 2;
|
|
y = h() / 2 - y;
|
|
|
|
LastMousePos.x = x;
|
|
LastMousePos.y = y;
|
|
|
|
// Don't go any further if the OpenGL context hasn't been
|
|
// initialized/updated by a draw()
|
|
if(!valid()) return 1;
|
|
|
|
if(event == FL_DRAG || event == FL_MOVE) {
|
|
int state = Fl::event_state();
|
|
SS.GW.MouseMoved(x, y,
|
|
state & FL_BUTTON1,
|
|
state & FL_BUTTON2,
|
|
state & FL_BUTTON3,
|
|
state & FL_SHIFT,
|
|
state & FL_CTRL);
|
|
return 1;
|
|
}
|
|
|
|
#if FL_RIGHT_MOUSE != 3
|
|
# error "MOUSE() macro may need revising"
|
|
#endif
|
|
#define MOUSE(btn,ev) (16 * ev + btn)
|
|
|
|
switch(MOUSE(Fl::event_button(), event))
|
|
{
|
|
case MOUSE(FL_LEFT_MOUSE, FL_PUSH):
|
|
if(Fl::event_clicks()) {
|
|
SS.GW.MouseLeftDoubleClick(x, y);
|
|
} else {
|
|
SS.GW.MouseLeftDown(x, y);
|
|
}
|
|
break;
|
|
|
|
case MOUSE(FL_LEFT_MOUSE, FL_RELEASE):
|
|
SS.GW.MouseLeftUp(x, y); break;
|
|
|
|
case MOUSE(FL_MIDDLE_MOUSE, FL_PUSH):
|
|
case MOUSE(FL_RIGHT_MOUSE, FL_PUSH):
|
|
SS.GW.MouseMiddleOrRightDown(x, y); break;
|
|
|
|
case MOUSE(FL_MIDDLE_MOUSE, FL_RELEASE):
|
|
/* Not used */ break;
|
|
|
|
case MOUSE(FL_RIGHT_MOUSE, FL_RELEASE):
|
|
SS.GW.MouseRightUp(x, y); break;
|
|
|
|
default: oops(); break;
|
|
}
|
|
#undef MOUSE
|
|
return 1;
|
|
}
|
|
|
|
case FL_ENTER:
|
|
return 1;
|
|
|
|
case FL_LEAVE:
|
|
SS.GW.MouseLeave();
|
|
return 1;
|
|
|
|
case FL_FOCUS:
|
|
return 1;
|
|
|
|
case FL_UNFOCUS:
|
|
return 1;
|
|
|
|
case FL_KEYDOWN: {
|
|
int key = Fl::event_key();
|
|
int c = key;
|
|
switch(key) {
|
|
case FL_Escape:
|
|
c = GraphicsWindow::ESCAPE_KEY;
|
|
break;
|
|
case FL_Delete:
|
|
c = GraphicsWindow::DELETE_KEY;
|
|
break;
|
|
case FL_Tab:
|
|
c = '\t';
|
|
break;
|
|
|
|
case FL_Back:
|
|
case FL_BackSpace:
|
|
c = '\b';
|
|
break;
|
|
}
|
|
if(key >= (FL_F+1) && key <= (FL_F+12)) {
|
|
c = GraphicsWindow::FUNCTION_KEY_BASE + (key - FL_F);
|
|
}
|
|
if(Fl::event_shift()) c |= GraphicsWindow::SHIFT_MASK;
|
|
if(Fl::event_ctrl()) c |= GraphicsWindow::CTRL_MASK;
|
|
|
|
if(SS.GW.KeyDown(c)) return 1;
|
|
|
|
// No accelerator; process the key as normal.
|
|
break;
|
|
}
|
|
|
|
case FL_KEYUP:
|
|
return 1;
|
|
|
|
case FL_CLOSE:
|
|
// GraphicsGlWnd does not receive this event; we intercept
|
|
// close events in WindowCloseHandler()
|
|
oops();
|
|
return 0;
|
|
|
|
case FL_MOUSEWHEEL:
|
|
SS.GW.MouseScroll(LastMousePos.x, LastMousePos.y, Fl::event_dy());
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
protected:
|
|
|
|
void draw_gl(void)
|
|
{
|
|
// Actually paint the window, with gl.
|
|
SS.GW.Paint();
|
|
GL_CHECK();
|
|
}
|
|
|
|
virtual void dummy(void);
|
|
};
|
|
|
|
void Graphics_Gl_Window::dummy(void)
|
|
{
|
|
// sop to Clang++'s -Wweak-vtables warning
|
|
}
|
|
|
|
void PaintGraphics(void)
|
|
{
|
|
GraphicsGlWnd->redraw();
|
|
}
|
|
|
|
void InvalidateGraphics(void)
|
|
{
|
|
GraphicsGlWnd->redraw();
|
|
}
|
|
|
|
void ToggleFullScreen(void)
|
|
{
|
|
#ifdef HAVE_FLTK_FULLSCREEN
|
|
if(GraphicsWnd->fullscreen_active()) {
|
|
GraphicsWnd->fullscreen_off();
|
|
} else {
|
|
GraphicsWndOldSize.resize(
|
|
GraphicsWnd->x(),
|
|
GraphicsWnd->y(),
|
|
GraphicsWnd->w(),
|
|
GraphicsWnd->h());
|
|
|
|
GraphicsWnd->fullscreen();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool FullScreenIsActive(void)
|
|
{
|
|
#ifdef HAVE_FLTK_FULLSCREEN
|
|
return GraphicsWnd->fullscreen_active();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void GetGraphicsWindowSize(int *w, int *h)
|
|
{
|
|
*w = GraphicsGlWnd->w();
|
|
*h = GraphicsGlWnd->h();
|
|
}
|
|
|
|
void ToggleMenuBar(void)
|
|
{
|
|
int y = 0;
|
|
|
|
MenuBarVisible = !MenuBarVisible;
|
|
|
|
// We hide the menu bar by expanding the GL area over it, instead of
|
|
// calling hide(). This way, F10/Alt+F/etc. remain usable.
|
|
|
|
if(MenuBarVisible) y = MenuBar->h();
|
|
|
|
GraphicsGlWnd->resize(
|
|
0, y,
|
|
GraphicsWnd->w(), GraphicsWnd->h() - y);
|
|
|
|
// Make GraphicsWnd forget about the previous sizes of its children, or
|
|
// else the menu bar will {dis,re}appear when the window is resized
|
|
GraphicsWnd->init_sizes();
|
|
}
|
|
|
|
bool MenuBarIsVisible(void)
|
|
{
|
|
return MenuBarVisible;
|
|
}
|
|
|
|
class Text_Gl_Window : public Fl_Gl_Window_Group
|
|
{
|
|
public:
|
|
|
|
Text_Gl_Window(int x, int y, int w, int h)
|
|
: Fl_Gl_Window_Group(x, y, w, h)
|
|
{
|
|
mode(FL_RGB | FL_DOUBLE);
|
|
}
|
|
|
|
int handle_gl(int event)
|
|
{
|
|
switch(event)
|
|
{
|
|
case FL_PUSH: // mouse button click
|
|
case FL_MOVE:
|
|
if(valid()) {
|
|
SS.TW.MouseEvent(
|
|
event == FL_PUSH && Fl::event_button() == FL_LEFT_MOUSE,
|
|
Fl::event_button1(),
|
|
Fl::event_x(), Fl::event_y());
|
|
}
|
|
return 1;
|
|
|
|
case FL_ENTER:
|
|
case FL_FOCUS:
|
|
return 1;
|
|
|
|
case FL_LEAVE:
|
|
SS.TW.MouseLeave();
|
|
return 1;
|
|
|
|
case FL_KEYDOWN:
|
|
case FL_KEYUP:
|
|
return GraphicsGlWnd->handle(event);
|
|
|
|
case FL_CLOSE:
|
|
// TextGlWnd does not receive this event; we intercept
|
|
// close events in WindowCloseHandler()
|
|
oops();
|
|
return 0;
|
|
|
|
case FL_MOUSEWHEEL:
|
|
return TextWndScrollBar->handle(event);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
protected:
|
|
|
|
void draw_gl(void)
|
|
{
|
|
// Actually paint the text window, with gl.
|
|
SS.TW.Paint();
|
|
GL_CHECK();
|
|
}
|
|
|
|
virtual void dummy(void);
|
|
};
|
|
|
|
void Text_Gl_Window::dummy(void)
|
|
{
|
|
// sop to Clang++'s -Wweak-vtables warning
|
|
}
|
|
|
|
void InvalidateText(void)
|
|
{
|
|
TextGlWnd->redraw();
|
|
}
|
|
|
|
void GetTextWindowSize(int *w, int *h)
|
|
{
|
|
*w = TextGlWnd->w();
|
|
*h = TextGlWnd->h();
|
|
}
|
|
|
|
static void EditControlCallback(Fl_Widget *w)
|
|
{
|
|
if(w == GraphicsEditControl) {
|
|
SS.GW.EditControlDone(GraphicsEditControl->value());
|
|
} else if(w == TextEditControl) {
|
|
SS.TW.EditControlDone(TextEditControl->value());
|
|
} else {
|
|
oops();
|
|
}
|
|
}
|
|
|
|
static void WindowCloseHandler(Fl_Window *wnd, void *data)
|
|
{
|
|
if(wnd == GraphicsWnd) {
|
|
SolveSpace::MenuFile(GraphicsWindow::MNU_EXIT);
|
|
}
|
|
else if(wnd == TextWnd) {
|
|
if(SS.GW.showTextWindow) {
|
|
GraphicsWindow::MenuView(GraphicsWindow::MNU_SHOW_TEXT_WND);
|
|
}
|
|
} else {
|
|
oops();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Common dialog routines, to open or save a file.
|
|
//-----------------------------------------------------------------------------
|
|
bool GetOpenFile(char *file, const char *defExtension, const char *selPattern)
|
|
{
|
|
#ifdef USE_FLTK_FILE_CHOOSER
|
|
char *f = fl_file_chooser(
|
|
"Open File",
|
|
selPattern,
|
|
file[0] ? file : NULL,
|
|
0);
|
|
if(strlen(f)+1 > MAX_PATH) return false;
|
|
strcpy(file, f);
|
|
return true;
|
|
#else
|
|
Fl_Native_File_Chooser fc;
|
|
fc.title("Open File");
|
|
fc.type(Fl_Native_File_Chooser::BROWSE_FILE);
|
|
if(file[0]) fc.preset_file(file);
|
|
fc.filter(selPattern);
|
|
fc.options(Fl_Native_File_Chooser::PREVIEW);
|
|
if(fc.show() != 0) return false;
|
|
if(strlen(fc.filename())+1 > MAX_PATH) return false;
|
|
strcpy(file, fc.filename());
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool GetSaveFile(char *file, const char *defExtension, const char *selPattern)
|
|
{
|
|
#ifdef USE_FLTK_FILE_CHOOSER
|
|
char *f = fl_file_chooser(
|
|
"Save File",
|
|
selPattern,
|
|
file[0] ? file : NULL,
|
|
0);
|
|
if(strlen(f)+1 > MAX_PATH) return false;
|
|
strcpy(file, f);
|
|
return true;
|
|
#else
|
|
Fl_Native_File_Chooser fc;
|
|
fc.title("Save File");
|
|
fc.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE);
|
|
if(file[0]) fc.preset_file(file);
|
|
fc.filter(selPattern);
|
|
fc.options(
|
|
Fl_Native_File_Chooser::NEW_FOLDER |
|
|
Fl_Native_File_Chooser::SAVEAS_CONFIRM);
|
|
if(fc.show() != 0) return false;
|
|
if(strlen(fc.filename())+1 > MAX_PATH) return false;
|
|
strcpy(file, fc.filename());
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
int SaveFileYesNoCancel(void)
|
|
{
|
|
int ycn[] = { SAVE_YES, SAVE_CANCEL, SAVE_NO };
|
|
int r;
|
|
|
|
fl_message_title("SolveSpace");
|
|
r = fl_choice(
|
|
"The program has changed since it was last saved.\n\n"
|
|
"Do you want to save the changes?",
|
|
"Yes",
|
|
"Cancel", // default
|
|
"No");
|
|
|
|
return ycn[r];
|
|
}
|
|
|
|
#ifndef HAVE_FONTCONFIG
|
|
static void ScanFontDirectory(const char *dir)
|
|
{
|
|
dirent **list = NULL;
|
|
char path[MAX_PATH];
|
|
|
|
int n = fl_filename_list(dir, &list, fl_alphasort);
|
|
if(n < 0)
|
|
return;
|
|
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
int len = fl_snprintf(path, sizeof(path), "%s/%s", dir, list[i]->d_name);
|
|
if(len >= MAX_PATH) continue;
|
|
|
|
if(fl_filename_isdir(path)) {
|
|
ScanFontDirectory(path);
|
|
}
|
|
else if(fl_filename_match(path, "*.{TTF,ttf}")) {
|
|
TtfFont tf;
|
|
ZERO(&tf);
|
|
strcpy(tf.fontFile, path);
|
|
SS.fonts.l.Add(&tf);
|
|
}
|
|
}
|
|
|
|
fl_filename_free_list(&list, n);
|
|
}
|
|
#endif // ndef HAVE_FONTCONFIG
|
|
|
|
void LoadAllFontFiles(void)
|
|
{
|
|
#ifdef HAVE_FONTCONFIG
|
|
|
|
if(!FcInit())
|
|
return;
|
|
|
|
FcPattern *pat = FcPatternCreate();
|
|
FcObjectSet *os = FcObjectSetBuild(FC_FILE, (char *)0);
|
|
FcFontSet *fs = FcFontList(0, pat, os);
|
|
|
|
for(int i = 0; i < fs->nfont; i++) {
|
|
char *s = FcPatternFormat(fs->fonts[i], "%{file}");
|
|
if(strlen(s)+1 <= MAX_PATH && fl_filename_match(s, "*.{TTF,ttf}") {
|
|
TtfFont tf;
|
|
ZERO(&tf);
|
|
strcpy(tf.fontFile, s);
|
|
SS.fonts.l.Add(&tf);
|
|
}
|
|
FcStrFree(s);
|
|
}
|
|
|
|
FcFontSetDestroy(fs);
|
|
FcObjectSetDestroy(os);
|
|
FcPatternDestroy(pat);
|
|
FcFini();
|
|
|
|
#else
|
|
|
|
# ifdef __APPLE__
|
|
ScanFontDirectory("/System/Library/Fonts");
|
|
ScanFontDirectory("/Library/Fonts");
|
|
# else
|
|
ScanFontDirectory("/usr/lib/X11/fonts");
|
|
ScanFontDirectory("/usr/openwin/lib/X11/fonts/TrueType");
|
|
ScanFontDirectory("/usr/share/fonts/truetype");
|
|
# endif
|
|
|
|
#endif
|
|
}
|
|
|
|
enum {
|
|
CHECK,
|
|
RADIO,
|
|
ACTIVE
|
|
};
|
|
|
|
static void MenuById(int id, bool yes, int what)
|
|
{
|
|
const Fl_Menu_Item *menu = MenuBar->menu();
|
|
int size = MenuBar->size();
|
|
|
|
for(int i = 0; i < size; i++) {
|
|
const Fl_Menu_Item *m = &menu[i];
|
|
if(m->submenu()) continue;
|
|
if(m->argument() != (long)id) continue;
|
|
|
|
int flags = MenuBar->mode(i);
|
|
switch(what) {
|
|
case CHECK:
|
|
if(!m->checkbox()) flags |= FL_MENU_TOGGLE;
|
|
if(yes) flags |= FL_MENU_VALUE;
|
|
else flags &= ~FL_MENU_VALUE;
|
|
break;
|
|
case RADIO:
|
|
if(!m->radio()) flags |= FL_MENU_RADIO;
|
|
if(yes) flags |= FL_MENU_VALUE;
|
|
else flags &= ~FL_MENU_VALUE;
|
|
break;
|
|
case ACTIVE:
|
|
if(yes) flags &= ~FL_MENU_INACTIVE;
|
|
else flags |= FL_MENU_INACTIVE;
|
|
break;
|
|
default: oops(); break;
|
|
}
|
|
MenuBar->mode(i, flags);
|
|
return;
|
|
}
|
|
oops();
|
|
}
|
|
|
|
void CheckMenuById(int id, bool checked)
|
|
{
|
|
MenuById(id, checked, CHECK);
|
|
}
|
|
|
|
void RadioMenuById(int id, bool selected)
|
|
{
|
|
MenuById(id, selected, RADIO);
|
|
}
|
|
|
|
void EnableMenuById(int id, bool enabled)
|
|
{
|
|
MenuById(id, enabled, ACTIVE);
|
|
}
|
|
|
|
static void RecentMenuCallback(Fl_Widget *w, long data)
|
|
{
|
|
int id = (int)data;
|
|
if((id >= RECENT_OPEN && id < (RECENT_OPEN + MAX_RECENT))) {
|
|
SolveSpace::MenuFile(id);
|
|
}
|
|
else if((id >= RECENT_IMPORT && id < (RECENT_IMPORT + MAX_RECENT))) {
|
|
Group::MenuGroup(id);
|
|
}
|
|
}
|
|
|
|
static void DoRecent(Fl_Menu_Item *m, int base)
|
|
{
|
|
int c = 0;
|
|
for(int i = 0; i < MAX_RECENT; i++) {
|
|
char *s = RecentFile[i];
|
|
if(*s) {
|
|
m[c].label(s);
|
|
m[c].callback(RecentMenuCallback);
|
|
m[c].argument(base + i);
|
|
c++;
|
|
}
|
|
}
|
|
if(c == 0) {
|
|
m[0].label("(no recent files)");
|
|
m[0].deactivate();
|
|
}
|
|
}
|
|
|
|
void RefreshRecentMenus(void)
|
|
{
|
|
ZERO(RecentOpenMenu);
|
|
ZERO(RecentImportMenu);
|
|
DoRecent(RecentOpenMenu, RECENT_OPEN);
|
|
DoRecent(RecentImportMenu, RECENT_IMPORT);
|
|
}
|
|
|
|
static void GraphicsWndMenuCallback(Fl_Widget *w, long data)
|
|
{
|
|
int id = (int)data;
|
|
|
|
for(int i = 0; SS.GW.menu[i].level >= 0; i++)
|
|
if(SS.GW.menu[i].id == id)
|
|
return SS.GW.menu[i].fn(id);
|
|
oops();
|
|
}
|
|
|
|
static void CreateGraphicsWindowMenus(void)
|
|
{
|
|
MenuBar = new Fl_Sys_Menu_Bar(0, 0, GraphicsWnd->w(), 100);
|
|
if(!MenuBar) oops();
|
|
|
|
RefreshRecentMenus();
|
|
ZERO(MenuBarItems);
|
|
|
|
int c = 0;
|
|
for(int i = 0; SS.GW.menu[i].level >= 0; i++) {
|
|
int accel = SS.GW.menu[i].accel;
|
|
int shortcut = accel & 0xff;
|
|
if(shortcut >= 'A' && shortcut <= 'Z') shortcut |= 0x20;
|
|
switch(shortcut) {
|
|
case GraphicsWindow::ESCAPE_KEY:
|
|
shortcut = FL_Escape;
|
|
break;
|
|
case GraphicsWindow::DELETE_KEY:
|
|
shortcut = FL_Delete;
|
|
break;
|
|
default:
|
|
if(accel & GraphicsWindow::SHIFT_MASK)
|
|
shortcut += FL_SHIFT;
|
|
if(accel & GraphicsWindow::CTRL_MASK)
|
|
shortcut += FL_CTRL;
|
|
break;
|
|
}
|
|
if(accel >= (GraphicsWindow::FUNCTION_KEY_BASE + 1) &&
|
|
accel <= (GraphicsWindow::FUNCTION_KEY_BASE + 12)) {
|
|
shortcut = FL_F + (accel - GraphicsWindow::FUNCTION_KEY_BASE);
|
|
}
|
|
|
|
Fl_Menu_Item *m = &MenuBarItems[c];
|
|
switch(SS.GW.menu[i].level) {
|
|
case 0:
|
|
m->label(SS.GW.menu[i].label);
|
|
m->shortcut(shortcut);
|
|
m->flags = FL_SUBMENU;
|
|
c++;
|
|
break;
|
|
|
|
case 1:
|
|
if(!SS.GW.menu[i].label) break; // divider
|
|
m->label(SS.GW.menu[i].label);
|
|
m->shortcut(shortcut);
|
|
switch(SS.GW.menu[i].id) {
|
|
case GraphicsWindow::MNU_OPEN_RECENT:
|
|
m->user_data(RecentOpenMenu);
|
|
m->flags = FL_SUBMENU_POINTER;
|
|
break;
|
|
case GraphicsWindow::MNU_GROUP_RECENT:
|
|
m->user_data(RecentImportMenu);
|
|
m->flags = FL_SUBMENU_POINTER;
|
|
break;
|
|
default:
|
|
m->callback(GraphicsWndMenuCallback);
|
|
m->argument(SS.GW.menu[i].id);
|
|
m->flags = SS.GW.menu[i+1].label
|
|
|| SS.GW.menu[i+1].level < 0 ? 0 : FL_MENU_DIVIDER;
|
|
break;
|
|
}
|
|
c++;
|
|
break;
|
|
|
|
default: oops(); break;
|
|
}
|
|
|
|
if(!SS.GW.menu[i+1].level) {
|
|
if(!SS.GW.menu[i].label) oops();
|
|
c++; // leave null item to end current submenu
|
|
}
|
|
}
|
|
|
|
// Make F10 bring up the File menu
|
|
MenuBarItems[0].shortcut(FL_F+10);
|
|
|
|
MenuBar->menu(MenuBarItems);
|
|
MenuBar->size(MenuBar->w(), 2 * MenuBar->textsize()); // fudge
|
|
MenuBar->global();
|
|
}
|
|
|
|
static void CreateMainWindows(void)
|
|
{
|
|
// Graphics window
|
|
|
|
GraphicsWnd = new Fl_Window(
|
|
3 * Fl::w() / 4, 3 * Fl::h() / 4,
|
|
"SolveSpace (not yet saved)");
|
|
if(!GraphicsWnd) oops();
|
|
|
|
CreateGraphicsWindowMenus();
|
|
|
|
// Avoid momentary grey flicker
|
|
GraphicsWnd->color(FL_BLACK);
|
|
|
|
GraphicsGlWnd = new Graphics_Gl_Window(
|
|
0, MenuBar->h(),
|
|
GraphicsWnd->w(), GraphicsWnd->h() - MenuBar->h());
|
|
|
|
GraphicsWnd->resizable(GraphicsGlWnd);
|
|
GraphicsWnd->size_range(Fl::w() / 4, Fl::h() / 4);
|
|
|
|
GraphicsEditControl = new Fl_Input(0, 20, 120, 30);
|
|
GraphicsEditControl->textfont(SS_FONT_MONOSPACE);
|
|
GraphicsEditControl->callback(EditControlCallback);
|
|
GraphicsEditControl->when(FL_WHEN_ENTER_KEY | FL_WHEN_NOT_CHANGED);
|
|
GraphicsEditControl->hide();
|
|
|
|
GraphicsGlWnd->end();
|
|
GraphicsWnd->end();
|
|
|
|
// Text window
|
|
|
|
TextWnd = new Fl_Window(480, 320, "SolveSpace - Browser");
|
|
if(!TextWnd) oops();
|
|
|
|
TextWnd->color(FL_BLACK);
|
|
|
|
TextWndScrollBar = new Fl_Scrollbar(
|
|
TextWnd->w() - Fl::scrollbar_size(), 0,
|
|
Fl::scrollbar_size(), TextWnd->h());
|
|
|
|
//TextWndScrollBar->value(0, 1, 0, 1);
|
|
TextWndScrollBar->callback(HandleTextWindowScrollBar);
|
|
|
|
TextGlWnd = new Text_Gl_Window(
|
|
0, 0,
|
|
TextWnd->w() - TextWndScrollBar->w(), TextWnd->h());
|
|
|
|
TextWnd->resizable(TextGlWnd);
|
|
TextWnd->size_range(Fl::w() / 8, Fl::h() / 8);
|
|
|
|
// We get the desired Alt+Tab behaviour by specifying that the text
|
|
// window is "non-modal".
|
|
|
|
TextWnd->set_non_modal();
|
|
|
|
TextEditControl = new Fl_Input(
|
|
0, 0,
|
|
20 * TextWindow::CHAR_WIDTH, TextWindow::LINE_HEIGHT);
|
|
TextEditControl->textfont(SS_FONT_MONOSPACE);
|
|
TextEditControl->callback(EditControlCallback);
|
|
TextEditControl->when(FL_WHEN_ENTER_KEY | FL_WHEN_NOT_CHANGED);
|
|
TextEditControl->hide();
|
|
|
|
TextGlWnd->end();
|
|
TextWnd->end();
|
|
|
|
Fl::set_atclose(WindowCloseHandler);
|
|
}
|
|
|
|
static void LoadFixedFont(void)
|
|
{
|
|
const char *names[] = {
|
|
"DejaVu Sans Mono",
|
|
"Bitstream Vera Sans Mono",
|
|
"Liberation Mono",
|
|
"monospace",
|
|
NULL
|
|
};
|
|
int i;
|
|
|
|
for (i = 0; names[i] != NULL; i++)
|
|
{
|
|
Fl::set_font(SS_FONT_MONOSPACE, names[i]);
|
|
fl_font(SS_FONT_MONOSPACE, 144);
|
|
if (fl_width("abcd1234") >= 1.0)
|
|
return;
|
|
}
|
|
|
|
oops();
|
|
}
|
|
|
|
static int ArgHandler(int argc, char **argv, int &i)
|
|
{
|
|
fprintf(stderr, "option %d = '%s'\n", i, argv[i]);
|
|
|
|
|
|
|
|
return 0;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Entry point into the program.
|
|
//-----------------------------------------------------------------------------
|
|
int main(int argc, char **argv)
|
|
{
|
|
// Parse command-line options
|
|
int optndx = 0;
|
|
if (!Fl::args(argc, argv, optndx, ArgHandler)) {
|
|
Fl::fatal(Fl::help);
|
|
}
|
|
|
|
// Initialize StartTimeSeconds
|
|
GetMilliseconds();
|
|
|
|
#ifndef USE_FLTK_FILE_CHOOSER
|
|
// The docs for Fl_Native_File_Chooser recommend doing this
|
|
Fl_File_Icon::load_system_icons();
|
|
#endif
|
|
|
|
// Don't make message dialogs show up under the mouse pointer
|
|
fl_message_hotspot(0);
|
|
|
|
LoadPreferences();
|
|
|
|
// A monospaced font
|
|
LoadFixedFont();
|
|
|
|
// Create the root windows: one for control, with text, and one for
|
|
// the graphics
|
|
CreateMainWindows();
|
|
|
|
CnfThawWindowPos(TextWnd, "TextWindow");
|
|
CnfThawWindowPos(GraphicsWnd, "GraphicsWindow");
|
|
|
|
GraphicsWnd->show(argc, argv);
|
|
ShowTextWindow(true);
|
|
|
|
// Don't use the default (FL_CURSOR_DEFAULT) arrow pointer, as it can't
|
|
// be resized by the user
|
|
SetMousePointerToHand(false);
|
|
|
|
// A filename may have been specified on the command line; if so, then
|
|
// make it absolute.
|
|
char file[MAX_PATH] = "";
|
|
if(optind < argc && strlen(argv[optind])+1 < MAX_PATH) {
|
|
strcpy(file, argv[optind]);
|
|
}
|
|
if(*file != '\0') {
|
|
GetAbsoluteFilename(file);
|
|
}
|
|
|
|
#ifdef HAVE_LIBSPNAV
|
|
bool spacenavd_active =
|
|
spnav_x11_open(fl_display, fl_xid(GraphicsWnd)) == 0;
|
|
#endif
|
|
|
|
// Call in to the platform-independent code, and let them do their init
|
|
SS.Init(file);
|
|
|
|
// And now it's the main event loop. All calls in to the rest of the
|
|
// code will be from the callbacks.
|
|
for(;;) {
|
|
// This call to Fl::first_window() ensures that Fl::flush() draws
|
|
// TextGlWnd before TextWnd, which allows MoveTextScrollbarTo()
|
|
// (normally called while TextGlWnd is being drawn) to update the
|
|
// text-window scrollbar (a child widget of TextWnd) immediately,
|
|
// rather than being delayed until the next redraw. This has to be
|
|
// done at every iteration because FLTK constantly updates the
|
|
// window order, placing those which received events most recently
|
|
// at the beginning of the list.
|
|
Fl::first_window(TextGlWnd);
|
|
if(!Fl::wait()) break;
|
|
// TODO: invoke DoLater() at the right time
|
|
SS.DoLater();
|
|
}
|
|
|
|
#ifdef HAVE_LIBSPNAV
|
|
if(spacenavd_active) {
|
|
spnav_close();
|
|
}
|
|
#endif
|
|
|
|
// Write everything back into preferences
|
|
CnfFreezeWindowPos(TextWnd, "TextWindow");
|
|
CnfFreezeWindowPos(
|
|
#ifdef HAVE_FLTK_FULLSCREEN
|
|
GraphicsWnd->fullscreen_active() ? &GraphicsWndOldSize : GraphicsWnd,
|
|
#else
|
|
GraphicsWnd,
|
|
#endif
|
|
"GraphicsWindow");
|
|
|
|
delete TextWnd;
|
|
delete GraphicsWnd;
|
|
delete Preferences;
|
|
|
|
// Free the memory we've used; anything that remains is a leak.
|
|
SK.Clear();
|
|
SS.Clear();
|
|
|
|
return 0;
|
|
}
|