
With this commit, SolveSpace gains an Autotools build system and a new platform-dependent backend implemented using the FLTK GUI toolkit. These will allow the application to be built and run on Linux and other Unix-like operating systems, and prospectively, MacOS X. A number of new files have been added: * Makefile.am: Automake makefile template; this contains some experimental support for MinGW and MSVC++ builds that needs further development * ac-aux/ax_fltk.m4: Autoconf M4 macro to locate and query the system's installation of FLTK; this will eventually be contributed to the GNU Autoconf Archive * autogen.sh: Script to bootstrap the Autotools build system, usually for a tree just checked out from source control * configure.ac: Source for the Autoconf configure script; note that this file specifies a version of 2.1, near the top * fltk/fltkmain.cpp: Main FLTK backend implementation * fltk/fltkutil.cpp: Utility functions for the FLTK backend * fltk/xFl_Gl_Window_Group.{H,cxx}: Implementation of a new Fl_Gl_Window_Group widget for FLTK, needed to facilitate drawing FLTK widgets on top of OpenGL graphics as SolveSpace does. This has been submitted to the FLTK project for (hopefully) eventual upstream inclusion: http://www.fltk.org/str.php?L2992 The following minor changes are also a part of this commit: * Makefile.msvc: Define PACKAGE_VERSION=2.1 for the benefit of solvespace.cpp in MSVC++ builds * solvespace.cpp: In the About dialog text, use PACKAGE_VERSION rather than hard-coding the version of the program * solvespace.h: Don't define the C99 integer types if HAVE_C99_INTEGER_TYPES is defined, to facilitate MinGW builds
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;
|
|
}
|