Implement a platform abstraction for menus.
This commit removes a large amount of redundant code that needed to be kept in sync between platforms and also makes it much easier to add new menu-related functionality since little to no platform code needs to be altered anymore. This commit also greatly improves code locality in context menu handling by allowing context menu click handlers to be closures. This commit temporarily introduces a SetMainMenu API, which is rather hacky but only necessary until an abstraction for windows is added.
This commit is contained in:
parent
7ab87caa88
commit
55baaf310f
@ -193,6 +193,7 @@ set(solvespace_core_SOURCES
|
||||
util.cpp
|
||||
view.cpp
|
||||
platform/platform.cpp
|
||||
platform/gui.cpp
|
||||
render/render.cpp
|
||||
render/render2d.cpp
|
||||
srf/boolean.cpp
|
||||
|
@ -6,6 +6,17 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "solvespace.h"
|
||||
|
||||
typedef void MenuHandler(Command id);
|
||||
using MenuKind = Platform::MenuItem::Indicator;
|
||||
struct MenuEntry {
|
||||
int level; // 0 == on menu bar, 1 == one level down
|
||||
const char *label; // or NULL for a separator
|
||||
Command cmd; // command ID
|
||||
int accel; // keyboard accelerator
|
||||
MenuKind kind;
|
||||
MenuHandler *fn;
|
||||
};
|
||||
|
||||
#define mView (&GraphicsWindow::MenuView)
|
||||
#define mEdit (&GraphicsWindow::MenuEdit)
|
||||
#define mClip (&GraphicsWindow::MenuClipboard)
|
||||
@ -15,195 +26,343 @@
|
||||
#define mGrp (&Group::MenuGroup)
|
||||
#define mAna (&SolveSpaceUI::MenuAnalyze)
|
||||
#define mHelp (&SolveSpaceUI::MenuHelp)
|
||||
#define DEL DELETE_KEY
|
||||
#define ESC ESCAPE_KEY
|
||||
#define SHIFT_MASK 0x100
|
||||
#define CTRL_MASK 0x200
|
||||
#define FN_MASK 0x400
|
||||
|
||||
#define S SHIFT_MASK
|
||||
#define C CTRL_MASK
|
||||
#define F(k) (FUNCTION_KEY_BASE+(k))
|
||||
#define TN MenuKind::NORMAL
|
||||
#define TC MenuKind::CHECK
|
||||
#define TR MenuKind::RADIO
|
||||
const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = {
|
||||
//level
|
||||
// label id accel ty fn
|
||||
{ 0, N_("&File"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&New"), Command::NEW, C|'N', TN, mFile },
|
||||
{ 1, N_("&Open..."), Command::OPEN, C|'O', TN, mFile },
|
||||
{ 1, N_("Open &Recent"), Command::OPEN_RECENT, 0, TN, mFile },
|
||||
{ 1, N_("&Save"), Command::SAVE, C|'S', TN, mFile },
|
||||
{ 1, N_("Save &As..."), Command::SAVE_AS, 0, TN, mFile },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Export &Image..."), Command::EXPORT_PNG, 0, TN, mFile },
|
||||
{ 1, N_("Export 2d &View..."), Command::EXPORT_VIEW, 0, TN, mFile },
|
||||
{ 1, N_("Export 2d &Section..."), Command::EXPORT_SECTION, 0, TN, mFile },
|
||||
{ 1, N_("Export 3d &Wireframe..."), Command::EXPORT_WIREFRAME, 0, TN, mFile },
|
||||
{ 1, N_("Export Triangle &Mesh..."), Command::EXPORT_MESH, 0, TN, mFile },
|
||||
{ 1, N_("Export &Surfaces..."), Command::EXPORT_SURFACES, 0, TN, mFile },
|
||||
{ 1, N_("Im&port..."), Command::IMPORT, 0, TN, mFile },
|
||||
#define F FN_MASK
|
||||
#define KN MenuKind::NONE
|
||||
#define KC MenuKind::CHECK_MARK
|
||||
#define KR MenuKind::RADIO_MARK
|
||||
const MenuEntry Menu[] = {
|
||||
//lv label cmd accel kind
|
||||
{ 0, N_("&File"), Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&New"), Command::NEW, C|'n', KN, mFile },
|
||||
{ 1, N_("&Open..."), Command::OPEN, C|'o', KN, mFile },
|
||||
{ 1, N_("Open &Recent"), Command::OPEN_RECENT, 0, KN, mFile },
|
||||
{ 1, N_("&Save"), Command::SAVE, C|'s', KN, mFile },
|
||||
{ 1, N_("Save &As..."), Command::SAVE_AS, 0, KN, mFile },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Export &Image..."), Command::EXPORT_PNG, 0, KN, mFile },
|
||||
{ 1, N_("Export 2d &View..."), Command::EXPORT_VIEW, 0, KN, mFile },
|
||||
{ 1, N_("Export 2d &Section..."), Command::EXPORT_SECTION, 0, KN, mFile },
|
||||
{ 1, N_("Export 3d &Wireframe..."), Command::EXPORT_WIREFRAME, 0, KN, mFile },
|
||||
{ 1, N_("Export Triangle &Mesh..."), Command::EXPORT_MESH, 0, KN, mFile },
|
||||
{ 1, N_("Export &Surfaces..."), Command::EXPORT_SURFACES, 0, KN, mFile },
|
||||
{ 1, N_("Im&port..."), Command::IMPORT, 0, KN, mFile },
|
||||
#ifndef __APPLE__
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("E&xit"), Command::EXIT, C|'Q', TN, mFile },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("E&xit"), Command::EXIT, C|'q', KN, mFile },
|
||||
#endif
|
||||
|
||||
{ 0, N_("&Edit"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Undo"), Command::UNDO, C|'Z', TN, mEdit },
|
||||
{ 1, N_("&Redo"), Command::REDO, C|'Y', TN, mEdit },
|
||||
{ 1, N_("Re&generate All"), Command::REGEN_ALL, ' ', TN, mEdit },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Snap Selection to &Grid"), Command::SNAP_TO_GRID, '.', TN, mEdit },
|
||||
{ 1, N_("Rotate Imported &90°"), Command::ROTATE_90, '9', TN, mEdit },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Cu&t"), Command::CUT, C|'X', TN, mClip },
|
||||
{ 1, N_("&Copy"), Command::COPY, C|'C', TN, mClip },
|
||||
{ 1, N_("&Paste"), Command::PASTE, C|'V', TN, mClip },
|
||||
{ 1, N_("Paste &Transformed..."), Command::PASTE_TRANSFORM, C|'T', TN, mClip },
|
||||
{ 1, N_("&Delete"), Command::DELETE, DEL, TN, mClip },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Select &Edge Chain"), Command::SELECT_CHAIN, C|'E', TN, mEdit },
|
||||
{ 1, N_("Select &All"), Command::SELECT_ALL, C|'A', TN, mEdit },
|
||||
{ 1, N_("&Unselect All"), Command::UNSELECT_ALL, ESC, TN, mEdit },
|
||||
{ 0, N_("&Edit"), Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&Undo"), Command::UNDO, C|'z', KN, mEdit },
|
||||
{ 1, N_("&Redo"), Command::REDO, C|'y', KN, mEdit },
|
||||
{ 1, N_("Re&generate All"), Command::REGEN_ALL, ' ', KN, mEdit },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Snap Selection to &Grid"), Command::SNAP_TO_GRID, '.', KN, mEdit },
|
||||
{ 1, N_("Rotate Imported &90°"), Command::ROTATE_90, '9', KN, mEdit },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Cu&t"), Command::CUT, C|'x', KN, mClip },
|
||||
{ 1, N_("&Copy"), Command::COPY, C|'c', KN, mClip },
|
||||
{ 1, N_("&Paste"), Command::PASTE, C|'v', KN, mClip },
|
||||
{ 1, N_("Paste &Transformed..."), Command::PASTE_TRANSFORM, C|'t', KN, mClip },
|
||||
{ 1, N_("&Delete"), Command::DELETE, '\x7f', KN, mClip },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Select &Edge Chain"), Command::SELECT_CHAIN, C|'e', KN, mEdit },
|
||||
{ 1, N_("Select &All"), Command::SELECT_ALL, C|'a', KN, mEdit },
|
||||
{ 1, N_("&Unselect All"), Command::UNSELECT_ALL, '\x1b', KN, mEdit },
|
||||
|
||||
{ 0, N_("&View"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Zoom &In"), Command::ZOOM_IN, '+', TN, mView },
|
||||
{ 1, N_("Zoom &Out"), Command::ZOOM_OUT, '-', TN, mView },
|
||||
{ 1, N_("Zoom To &Fit"), Command::ZOOM_TO_FIT, 'F', TN, mView },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Align View to &Workplane"), Command::ONTO_WORKPLANE, 'W', TN, mView },
|
||||
{ 1, N_("Nearest &Ortho View"), Command::NEAREST_ORTHO, F(2), TN, mView },
|
||||
{ 1, N_("Nearest &Isometric View"), Command::NEAREST_ISO, F(3), TN, mView },
|
||||
{ 1, N_("&Center View At Point"), Command::CENTER_VIEW, F(4), TN, mView },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Show Snap &Grid"), Command::SHOW_GRID, '>', TC, mView },
|
||||
{ 1, N_("Use &Perspective Projection"), Command::PERSPECTIVE_PROJ, '`', TC, mView },
|
||||
{ 1, N_("Dimension &Units"), Command::NONE, 0, TN, NULL },
|
||||
{ 2, N_("Dimensions in &Inches"), Command::UNITS_INCHES, 0, TR, mView },
|
||||
{ 2, N_("Dimensions in &Millimeters"), Command::UNITS_MM, 0, TR, mView },
|
||||
{ 2, N_("Dimensions in M&eters"), Command::UNITS_METERS, 0, TR, mView },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Show &Toolbar"), Command::SHOW_TOOLBAR, 0, TC, mView },
|
||||
{ 1, N_("Show Property Bro&wser"), Command::SHOW_TEXT_WND, '\t', TC, mView },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Full Screen"), Command::FULL_SCREEN, C|F(11), TC, mView },
|
||||
{ 0, N_("&View"), Command::NONE, 0, KN, mView },
|
||||
{ 1, N_("Zoom &In"), Command::ZOOM_IN, '+', KN, mView },
|
||||
{ 1, N_("Zoom &Out"), Command::ZOOM_OUT, '-', KN, mView },
|
||||
{ 1, N_("Zoom To &Fit"), Command::ZOOM_TO_FIT, 'f', KN, mView },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Align View to &Workplane"), Command::ONTO_WORKPLANE, 'w', KN, mView },
|
||||
{ 1, N_("Nearest &Ortho View"), Command::NEAREST_ORTHO, F|2, KN, mView },
|
||||
{ 1, N_("Nearest &Isometric View"), Command::NEAREST_ISO, F|3, KN, mView },
|
||||
{ 1, N_("&Center View At Point"), Command::CENTER_VIEW, F|4, KN, mView },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Show Snap &Grid"), Command::SHOW_GRID, '>', KC, mView },
|
||||
{ 1, N_("Use &Perspective Projection"), Command::PERSPECTIVE_PROJ, '`', KC, mView },
|
||||
{ 1, N_("Dimension &Units"), Command::NONE, 0, KN, NULL },
|
||||
{ 2, N_("Dimensions in &Millimeters"), Command::UNITS_MM, 0, KR, mView },
|
||||
{ 2, N_("Dimensions in M&eters"), Command::UNITS_METERS, 0, KR, mView },
|
||||
{ 2, N_("Dimensions in &Inches"), Command::UNITS_INCHES, 0, KR, mView },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Show &Toolbar"), Command::SHOW_TOOLBAR, 0, KC, mView },
|
||||
{ 1, N_("Show Property Bro&wser"), Command::SHOW_TEXT_WND, '\t', KC, mView },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&Full Screen"), Command::FULL_SCREEN, C|F|11, KC, mView },
|
||||
|
||||
{ 0, N_("&New Group"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Sketch In &3d"), Command::GROUP_3D, S|'3', TN, mGrp },
|
||||
{ 1, N_("Sketch In New &Workplane"), Command::GROUP_WRKPL, S|'W', TN, mGrp },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Step &Translating"), Command::GROUP_TRANS, S|'T', TN, mGrp },
|
||||
{ 1, N_("Step &Rotating"), Command::GROUP_ROT, S|'R', TN, mGrp },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("E&xtrude"), Command::GROUP_EXTRUDE, S|'X', TN, mGrp },
|
||||
{ 1, N_("&Lathe"), Command::GROUP_LATHE, S|'L', TN, mGrp },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Link / Assemble..."), Command::GROUP_LINK, S|'I', TN, mGrp },
|
||||
{ 1, N_("Link Recent"), Command::GROUP_RECENT, 0, TN, mGrp },
|
||||
{ 0, N_("&New Group"), Command::NONE, 0, KN, mGrp },
|
||||
{ 1, N_("Sketch In &3d"), Command::GROUP_3D, S|'3', KN, mGrp },
|
||||
{ 1, N_("Sketch In New &Workplane"), Command::GROUP_WRKPL, S|'w', KN, mGrp },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Step &Translating"), Command::GROUP_TRANS, S|'t', KN, mGrp },
|
||||
{ 1, N_("Step &Rotating"), Command::GROUP_ROT, S|'r', KN, mGrp },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("E&xtrude"), Command::GROUP_EXTRUDE, S|'x', KN, mGrp },
|
||||
{ 1, N_("&Lathe"), Command::GROUP_LATHE, S|'l', KN, mGrp },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Link / Assemble..."), Command::GROUP_LINK, S|'i', KN, mGrp },
|
||||
{ 1, N_("Link Recent"), Command::GROUP_RECENT, 0, KN, mGrp },
|
||||
|
||||
{ 0, N_("&Sketch"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("In &Workplane"), Command::SEL_WORKPLANE, '2', TR, mReq },
|
||||
{ 1, N_("Anywhere In &3d"), Command::FREE_IN_3D, '3', TR, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Datum &Point"), Command::DATUM_POINT, 'P', TN, mReq },
|
||||
{ 1, N_("&Workplane"), Command::WORKPLANE, 0, TN, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Line &Segment"), Command::LINE_SEGMENT, 'S', TN, mReq },
|
||||
{ 1, N_("C&onstruction Line Segment"), Command::CONSTR_SEGMENT, S|'S', TN, mReq },
|
||||
{ 1, N_("&Rectangle"), Command::RECTANGLE, 'R', TN, mReq },
|
||||
{ 1, N_("&Circle"), Command::CIRCLE, 'C', TN, mReq },
|
||||
{ 1, N_("&Arc of a Circle"), Command::ARC, 'A', TN, mReq },
|
||||
{ 1, N_("&Bezier Cubic Spline"), Command::CUBIC, 'B', TN, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Text in TrueType Font"), Command::TTF_TEXT, 'T', TN, mReq },
|
||||
{ 1, N_("&Image"), Command::IMAGE, 0, TN, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("To&ggle Construction"), Command::CONSTRUCTION, 'G', TN, mReq },
|
||||
{ 1, N_("Tangent &Arc at Point"), Command::TANGENT_ARC, S|'A', TN, mReq },
|
||||
{ 1, N_("Split Curves at &Intersection"), Command::SPLIT_CURVES, 'I', TN, mReq },
|
||||
{ 0, N_("&Sketch"), Command::NONE, 0, KN, mReq },
|
||||
{ 1, N_("In &Workplane"), Command::SEL_WORKPLANE, '2', KR, mReq },
|
||||
{ 1, N_("Anywhere In &3d"), Command::FREE_IN_3D, '3', KR, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Datum &Point"), Command::DATUM_POINT, 'p', KN, mReq },
|
||||
{ 1, N_("&Workplane"), Command::WORKPLANE, 0, KN, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Line &Segment"), Command::LINE_SEGMENT, 's', KN, mReq },
|
||||
{ 1, N_("C&onstruction Line Segment"), Command::CONSTR_SEGMENT, S|'s', KN, mReq },
|
||||
{ 1, N_("&Rectangle"), Command::RECTANGLE, 'r', KN, mReq },
|
||||
{ 1, N_("&Circle"), Command::CIRCLE, 'c', KN, mReq },
|
||||
{ 1, N_("&Arc of a Circle"), Command::ARC, 'a', KN, mReq },
|
||||
{ 1, N_("&Bezier Cubic Spline"), Command::CUBIC, 'b', KN, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&Text in TrueType Font"), Command::TTF_TEXT, 't', KN, mReq },
|
||||
{ 1, N_("&Image"), Command::IMAGE, 0, KN, mReq },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("To&ggle Construction"), Command::CONSTRUCTION, 'g', KN, mReq },
|
||||
{ 1, N_("Tangent &Arc at Point"), Command::TANGENT_ARC, S|'a', KN, mReq },
|
||||
{ 1, N_("Split Curves at &Intersection"), Command::SPLIT_CURVES, 'i', KN, mReq },
|
||||
|
||||
{ 0, N_("&Constrain"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Distance / Diameter"), Command::DISTANCE_DIA, 'D', TN, mCon },
|
||||
{ 1, N_("Re&ference Dimension"), Command::REF_DISTANCE, S|'D', TN, mCon },
|
||||
{ 1, N_("A&ngle"), Command::ANGLE, 'N', TN, mCon },
|
||||
{ 1, N_("Reference An&gle"), Command::REF_ANGLE, S|'N', TN, mCon },
|
||||
{ 1, N_("Other S&upplementary Angle"), Command::OTHER_ANGLE, 'U', TN, mCon },
|
||||
{ 1, N_("Toggle R&eference Dim"), Command::REFERENCE, 'E', TN, mCon },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Horizontal"), Command::HORIZONTAL, 'H', TN, mCon },
|
||||
{ 1, N_("&Vertical"), Command::VERTICAL, 'V', TN, mCon },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&On Point / Curve / Plane"), Command::ON_ENTITY, 'O', TN, mCon },
|
||||
{ 1, N_("E&qual Length / Radius / Angle"), Command::EQUAL, 'Q', TN, mCon },
|
||||
{ 1, N_("Length Ra&tio"), Command::RATIO, 'Z', TN, mCon },
|
||||
{ 1, N_("Length Diff&erence"), Command::DIFFERENCE, 'J', TN, mCon },
|
||||
{ 1, N_("At &Midpoint"), Command::AT_MIDPOINT, 'M', TN, mCon },
|
||||
{ 1, N_("S&ymmetric"), Command::SYMMETRIC, 'Y', TN, mCon },
|
||||
{ 1, N_("Para&llel / Tangent"), Command::PARALLEL, 'L', TN, mCon },
|
||||
{ 1, N_("&Perpendicular"), Command::PERPENDICULAR, '[', TN, mCon },
|
||||
{ 1, N_("Same Orient&ation"), Command::ORIENTED_SAME, 'X', TN, mCon },
|
||||
{ 1, N_("Lock Point Where &Dragged"), Command::WHERE_DRAGGED, ']', TN, mCon },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Comment"), Command::COMMENT, ';', TN, mCon },
|
||||
{ 0, N_("&Constrain"), Command::NONE, 0, KN, mCon },
|
||||
{ 1, N_("&Distance / Diameter"), Command::DISTANCE_DIA, 'd', KN, mCon },
|
||||
{ 1, N_("Re&ference Dimension"), Command::REF_DISTANCE, S|'d', KN, mCon },
|
||||
{ 1, N_("A&ngle"), Command::ANGLE, 'n', KN, mCon },
|
||||
{ 1, N_("Reference An&gle"), Command::REF_ANGLE, S|'n', KN, mCon },
|
||||
{ 1, N_("Other S&upplementary Angle"), Command::OTHER_ANGLE, 'u', KN, mCon },
|
||||
{ 1, N_("Toggle R&eference Dim"), Command::REFERENCE, 'e', KN, mCon },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&Horizontal"), Command::HORIZONTAL, 'h', KN, mCon },
|
||||
{ 1, N_("&Vertical"), Command::VERTICAL, 'v', KN, mCon },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&On Point / Curve / Plane"), Command::ON_ENTITY, 'o', KN, mCon },
|
||||
{ 1, N_("E&qual Length / Radius / Angle"), Command::EQUAL, 'q', KN, mCon },
|
||||
{ 1, N_("Length Ra&tio"), Command::RATIO, 'z', KN, mCon },
|
||||
{ 1, N_("Length Diff&erence"), Command::DIFFERENCE, 'j', KN, mCon },
|
||||
{ 1, N_("At &Midpoint"), Command::AT_MIDPOINT, 'm', KN, mCon },
|
||||
{ 1, N_("S&ymmetric"), Command::SYMMETRIC, 'y', KN, mCon },
|
||||
{ 1, N_("Para&llel / Tangent"), Command::PARALLEL, 'l', KN, mCon },
|
||||
{ 1, N_("&Perpendicular"), Command::PERPENDICULAR, '[', KN, mCon },
|
||||
{ 1, N_("Same Orient&ation"), Command::ORIENTED_SAME, 'x', KN, mCon },
|
||||
{ 1, N_("Lock Point Where &Dragged"), Command::WHERE_DRAGGED, ']', KN, mCon },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Comment"), Command::COMMENT, ';', KN, mCon },
|
||||
|
||||
{ 0, N_("&Analyze"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Measure &Volume"), Command::VOLUME, C|S|'V', TN, mAna },
|
||||
{ 1, N_("Measure A&rea"), Command::AREA, C|S|'A', TN, mAna },
|
||||
{ 1, N_("Measure &Perimeter"), Command::PERIMETER, C|S|'P', TN, mAna },
|
||||
{ 1, N_("Show &Interfering Parts"), Command::INTERFERENCE, C|S|'I', TN, mAna },
|
||||
{ 1, N_("Show &Naked Edges"), Command::NAKED_EDGES, C|S|'N', TN, mAna },
|
||||
{ 1, N_("Show &Center of Mass"), Command::CENTER_OF_MASS, C|S|'C', TN, mAna },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("Show Degrees of &Freedom"), Command::SHOW_DOF, C|S|'F', TN, mAna },
|
||||
{ 1, NULL, Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Trace Point"), Command::TRACE_PT, C|S|'T', TN, mAna },
|
||||
{ 1, N_("&Stop Tracing..."), Command::STOP_TRACING, C|S|'S', TN, mAna },
|
||||
{ 1, N_("Step &Dimension..."), Command::STEP_DIM, C|S|'D', TN, mAna },
|
||||
{ 0, N_("&Analyze"), Command::NONE, 0, KN, mAna },
|
||||
{ 1, N_("Measure &Volume"), Command::VOLUME, C|S|'v', KN, mAna },
|
||||
{ 1, N_("Measure A&rea"), Command::AREA, C|S|'a', KN, mAna },
|
||||
{ 1, N_("Measure &Perimeter"), Command::PERIMETER, C|S|'p', KN, mAna },
|
||||
{ 1, N_("Show &Interfering Parts"), Command::INTERFERENCE, C|S|'i', KN, mAna },
|
||||
{ 1, N_("Show &Naked Edges"), Command::NAKED_EDGES, C|S|'n', KN, mAna },
|
||||
{ 1, N_("Show &Center of Mass"), Command::CENTER_OF_MASS, C|S|'c', KN, mAna },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("Show Degrees of &Freedom"), Command::SHOW_DOF, C|S|'f', KN, mAna },
|
||||
{ 1, NULL, Command::NONE, 0, KN, NULL },
|
||||
{ 1, N_("&Trace Point"), Command::TRACE_PT, C|S|'t', KN, mAna },
|
||||
{ 1, N_("&Stop Tracing..."), Command::STOP_TRACING, C|S|'s', KN, mAna },
|
||||
{ 1, N_("Step &Dimension..."), Command::STEP_DIM, C|S|'d', KN, mAna },
|
||||
|
||||
{ 0, N_("&Help"), Command::NONE, 0, TN, NULL },
|
||||
{ 1, N_("&Website / Manual"), Command::WEBSITE, 0, TN, mHelp },
|
||||
{ 1, N_("&Language"), Command::LOCALE, 0, TN, mHelp },
|
||||
{ 0, N_("&Help"), Command::NONE, 0, KN, mHelp },
|
||||
{ 1, N_("&Language"), Command::LOCALE, 0, KN, mHelp },
|
||||
{ 1, N_("&Website / Manual"), Command::WEBSITE, 0, KN, mHelp },
|
||||
#ifndef __APPLE__
|
||||
{ 1, N_("&About"), Command::ABOUT, 0, TN, mHelp },
|
||||
{ 1, N_("&About"), Command::ABOUT, 0, KN, mHelp },
|
||||
#endif
|
||||
{ -1, 0, Command::NONE, 0, TN, 0 }
|
||||
{ -1, 0, Command::NONE, 0, KN, NULL }
|
||||
};
|
||||
|
||||
#undef DEL
|
||||
#undef ESC
|
||||
#undef S
|
||||
#undef C
|
||||
#undef F
|
||||
#undef TN
|
||||
#undef TC
|
||||
#undef TR
|
||||
#undef KN
|
||||
#undef KC
|
||||
#undef KR
|
||||
|
||||
std::string SolveSpace::MakeAcceleratorLabel(int accel) {
|
||||
if(!accel) return "";
|
||||
void GraphicsWindow::ActivateCommand(Command cmd) {
|
||||
for(int i = 0; Menu[i].level >= 0; i++) {
|
||||
if(cmd == Menu[i].cmd) {
|
||||
(Menu[i].fn)((Command)Menu[i].cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string label;
|
||||
if(accel & GraphicsWindow::CTRL_MASK) {
|
||||
label += "Ctrl+";
|
||||
Platform::KeyboardEvent GraphicsWindow::AcceleratorForCommand(Command cmd) {
|
||||
int rawAccel = 0;
|
||||
for(int i = 0; Menu[i].level >= 0; i++) {
|
||||
if(cmd == Menu[i].cmd) {
|
||||
rawAccel = Menu[i].accel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(accel & GraphicsWindow::SHIFT_MASK) {
|
||||
label += "Shift+";
|
||||
|
||||
Platform::KeyboardEvent accel = {};
|
||||
if(rawAccel & SHIFT_MASK) {
|
||||
accel.shiftDown = true;
|
||||
}
|
||||
accel &= ~(GraphicsWindow::CTRL_MASK | GraphicsWindow::SHIFT_MASK);
|
||||
if(accel >= GraphicsWindow::FUNCTION_KEY_BASE + 1 &&
|
||||
accel <= GraphicsWindow::FUNCTION_KEY_BASE + 12) {
|
||||
label += ssprintf("F%d", accel - GraphicsWindow::FUNCTION_KEY_BASE);
|
||||
} else if(accel == '\t') {
|
||||
label += "Tab";
|
||||
} else if(accel == ' ') {
|
||||
label += "Space";
|
||||
} else if(accel == GraphicsWindow::ESCAPE_KEY) {
|
||||
label += "Esc";
|
||||
} else if(accel == GraphicsWindow::DELETE_KEY) {
|
||||
label += "Del";
|
||||
if(rawAccel & CTRL_MASK) {
|
||||
accel.controlDown = true;
|
||||
}
|
||||
if(rawAccel & FN_MASK) {
|
||||
accel.key = Platform::KeyboardEvent::Key::FUNCTION;
|
||||
accel.num = rawAccel & 0xff;
|
||||
} else {
|
||||
label += (char)(accel & 0xff);
|
||||
accel.key = Platform::KeyboardEvent::Key::CHARACTER;
|
||||
accel.chr = (char)(rawAccel & 0xff);
|
||||
}
|
||||
return label;
|
||||
|
||||
return accel;
|
||||
}
|
||||
|
||||
bool GraphicsWindow::KeyboardEvent(Platform::KeyboardEvent event) {
|
||||
using Platform::KeyboardEvent;
|
||||
|
||||
if(event.type == KeyboardEvent::Type::RELEASE)
|
||||
return true;
|
||||
|
||||
if(event.key == KeyboardEvent::Key::CHARACTER) {
|
||||
if(event.chr == '\b') {
|
||||
// Treat backspace identically to escape.
|
||||
MenuEdit(Command::UNSELECT_ALL);
|
||||
return true;
|
||||
} else if(event.chr == '=') {
|
||||
// Treat = as +. This is specific to US (and US-compatible) keyboard layouts,
|
||||
// but makes zooming from keyboard much more usable on these.
|
||||
// Ideally we'd have a platform-independent way of binding to a particular
|
||||
// physical key regardless of shift status...
|
||||
MenuView(Command::ZOOM_IN);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// On some platforms, the OS does not handle some or all keyboard accelerators,
|
||||
// so handle them here.
|
||||
for(int i = 0; Menu[i].level >= 0; i++) {
|
||||
if(AcceleratorForCommand(Menu[i].cmd).Equals(event)) {
|
||||
ActivateCommand(Menu[i].cmd);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void GraphicsWindow::PopulateMainMenu() {
|
||||
bool unique = false;
|
||||
mainMenu = Platform::GetOrCreateMainMenu(&unique);
|
||||
if(unique) mainMenu->Clear();
|
||||
|
||||
Platform::MenuRef currentSubMenu;
|
||||
std::vector<Platform::MenuRef> subMenuStack;
|
||||
for(int i = 0; Menu[i].level >= 0; i++) {
|
||||
while(Menu[i].level > 0 && Menu[i].level <= (int)subMenuStack.size()) {
|
||||
currentSubMenu = subMenuStack.back();
|
||||
subMenuStack.pop_back();
|
||||
}
|
||||
|
||||
if(Menu[i].label == NULL) {
|
||||
currentSubMenu->AddSeparator();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string label = Translate(Menu[i].label);
|
||||
if(Menu[i].level == 0) {
|
||||
currentSubMenu = mainMenu->AddSubMenu(label);
|
||||
} else if(Menu[i].cmd == Command::OPEN_RECENT) {
|
||||
openRecentMenu = currentSubMenu->AddSubMenu(label);
|
||||
} else if(Menu[i].cmd == Command::GROUP_RECENT) {
|
||||
linkRecentMenu = currentSubMenu->AddSubMenu(label);
|
||||
} else if(Menu[i].cmd == Command::LOCALE) {
|
||||
Platform::MenuRef localeMenu = currentSubMenu->AddSubMenu(label);
|
||||
for(const Locale &locale : Locales()) {
|
||||
localeMenu->AddItem(locale.displayName, [&]() {
|
||||
SetLocale(locale.Culture());
|
||||
CnfFreezeString(locale.Culture(), "Locale");
|
||||
|
||||
SS.UpdateWindowTitle();
|
||||
PopulateMainMenu();
|
||||
});
|
||||
}
|
||||
} else if(Menu[i].fn == NULL) {
|
||||
subMenuStack.push_back(currentSubMenu);
|
||||
currentSubMenu = currentSubMenu->AddSubMenu(label);
|
||||
} else {
|
||||
Platform::MenuItemRef menuItem = currentSubMenu->AddItem(label);
|
||||
menuItem->SetIndicator(Menu[i].kind);
|
||||
if(Menu[i].accel != 0) {
|
||||
menuItem->SetAccelerator(AcceleratorForCommand(Menu[i].cmd));
|
||||
}
|
||||
menuItem->onTrigger = std::bind(Menu[i].fn, Menu[i].cmd);
|
||||
|
||||
if(Menu[i].cmd == Command::SHOW_GRID) {
|
||||
showGridMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::PERSPECTIVE_PROJ) {
|
||||
perspectiveProjMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::SHOW_TOOLBAR) {
|
||||
showToolbarMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::SHOW_TEXT_WND) {
|
||||
showTextWndMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::FULL_SCREEN) {
|
||||
fullScreenMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::UNITS_MM) {
|
||||
unitsMmMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::UNITS_METERS) {
|
||||
unitsMetersMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::UNITS_INCHES) {
|
||||
unitsInchesMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::SEL_WORKPLANE) {
|
||||
inWorkplaneMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::FREE_IN_3D) {
|
||||
in3dMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::UNDO) {
|
||||
undoMenuItem = menuItem;
|
||||
} else if(Menu[i].cmd == Command::REDO) {
|
||||
redoMenuItem = menuItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopulateRecentFiles();
|
||||
SS.UndoEnableMenus();
|
||||
|
||||
SetMainMenu(mainMenu);
|
||||
}
|
||||
|
||||
static void PopulateMenuWithPathnames(Platform::MenuRef menu,
|
||||
std::vector<Platform::Path> pathnames,
|
||||
std::function<void(const Platform::Path &)> onTrigger) {
|
||||
menu->Clear();
|
||||
if(pathnames.empty()) {
|
||||
Platform::MenuItemRef menuItem = menu->AddItem(_("(no recent files)"));
|
||||
menuItem->SetEnabled(false);
|
||||
} else {
|
||||
for(Platform::Path pathname : pathnames) {
|
||||
Platform::MenuItemRef menuItem = menu->AddItem(pathname.raw);
|
||||
menuItem->onTrigger = [=]() { onTrigger(pathname); };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsWindow::PopulateRecentFiles() {
|
||||
PopulateMenuWithPathnames(openRecentMenu, SS.recentFiles, [](const Platform::Path &path) {
|
||||
if(!SS.OkayToStartNewFile()) return;
|
||||
SS.Load(path);
|
||||
});
|
||||
|
||||
PopulateMenuWithPathnames(linkRecentMenu, SS.recentFiles, [](const Platform::Path &path) {
|
||||
Group::MenuGroup(Command::GROUP_LINK, path);
|
||||
});
|
||||
}
|
||||
|
||||
void GraphicsWindow::Init() {
|
||||
PopulateMainMenu();
|
||||
|
||||
canvas = CreateRenderer();
|
||||
if(canvas) {
|
||||
persistentCanvas = canvas->CreateBatch();
|
||||
@ -670,31 +829,31 @@ void GraphicsWindow::EnsureValidActives() {
|
||||
|
||||
// And update the checked state for various menus
|
||||
bool locked = LockedInWorkplane();
|
||||
RadioMenuByCmd(Command::FREE_IN_3D, !locked);
|
||||
RadioMenuByCmd(Command::SEL_WORKPLANE, locked);
|
||||
in3dMenuItem->SetActive(!locked);
|
||||
inWorkplaneMenuItem->SetActive(locked);
|
||||
|
||||
SS.UndoEnableMenus();
|
||||
|
||||
switch(SS.viewUnits) {
|
||||
case Unit::MM:
|
||||
case Unit::INCHES:
|
||||
case Unit::METERS:
|
||||
case Unit::INCHES:
|
||||
break;
|
||||
default:
|
||||
SS.viewUnits = Unit::MM;
|
||||
break;
|
||||
}
|
||||
RadioMenuByCmd(Command::UNITS_MM, SS.viewUnits == Unit::MM);
|
||||
RadioMenuByCmd(Command::UNITS_METERS, SS.viewUnits == Unit::METERS);
|
||||
RadioMenuByCmd(Command::UNITS_INCHES, SS.viewUnits == Unit::INCHES);
|
||||
unitsMmMenuItem->SetActive(SS.viewUnits == Unit::MM);
|
||||
unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS);
|
||||
unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES);
|
||||
|
||||
ShowTextWindow(SS.GW.showTextWindow);
|
||||
CheckMenuByCmd(Command::SHOW_TEXT_WND, /*checked=*/SS.GW.showTextWindow);
|
||||
showTextWndMenuItem->SetActive(SS.GW.showTextWindow);
|
||||
|
||||
CheckMenuByCmd(Command::SHOW_TOOLBAR, /*checked=*/SS.showToolbar);
|
||||
CheckMenuByCmd(Command::PERSPECTIVE_PROJ, /*checked=*/SS.usePerspectiveProj);
|
||||
CheckMenuByCmd(Command::SHOW_GRID,/*checked=*/SS.GW.showSnapGrid);
|
||||
CheckMenuByCmd(Command::FULL_SCREEN, /*checked=*/FullScreenIsActive());
|
||||
showGridMenuItem->SetActive(SS.GW.showSnapGrid);
|
||||
perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj);
|
||||
showToolbarMenuItem->SetActive(SS.showToolbar);
|
||||
fullScreenMenuItem->SetActive(FullScreenIsActive());
|
||||
|
||||
if(change) SS.ScheduleShowTW();
|
||||
}
|
||||
@ -717,7 +876,7 @@ bool GraphicsWindow::LockedInWorkplane() {
|
||||
void GraphicsWindow::ForceTextWindowShown() {
|
||||
if(!showTextWindow) {
|
||||
showTextWindow = true;
|
||||
CheckMenuByCmd(Command::SHOW_TEXT_WND, /*checked=*/true);
|
||||
showTextWndMenuItem->SetActive(true);
|
||||
ShowTextWindow(true);
|
||||
}
|
||||
}
|
||||
|
@ -69,17 +69,16 @@ void Group::ExtrusionForceVectorTo(const Vector &v) {
|
||||
SK.GetParam(h.param(2))->val = v.z;
|
||||
}
|
||||
|
||||
void Group::MenuGroup(Command id) {
|
||||
void Group::MenuGroup(Command id) {
|
||||
MenuGroup(id, Platform::Path());
|
||||
}
|
||||
|
||||
void Group::MenuGroup(Command id, Platform::Path linkFile) {
|
||||
Group g = {};
|
||||
g.visible = true;
|
||||
g.color = RGBi(100, 100, 100);
|
||||
g.scale = 1;
|
||||
|
||||
if((uint32_t)id >= (uint32_t)Command::RECENT_LINK &&
|
||||
(uint32_t)id < ((uint32_t)Command::RECENT_LINK + MAX_RECENT)) {
|
||||
g.linkFile = RecentFile[(uint32_t)id-(uint32_t)Command::RECENT_LINK];
|
||||
id = Command::GROUP_LINK;
|
||||
}
|
||||
g.linkFile = linkFile;
|
||||
|
||||
SS.GW.GroupSelection();
|
||||
auto const &gs = SS.GW.gs;
|
||||
|
436
src/mouse.cpp
436
src/mouse.cpp
@ -507,24 +507,6 @@ void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) {
|
||||
orig.startedMoving = false;
|
||||
}
|
||||
|
||||
void GraphicsWindow::ContextMenuListStyles() {
|
||||
CreateContextSubmenu();
|
||||
Style *s;
|
||||
bool empty = true;
|
||||
for(s = SK.style.First(); s; s = SK.style.NextAfter(s)) {
|
||||
if(s->h.v < Style::FIRST_CUSTOM) continue;
|
||||
|
||||
AddContextMenuItem(s->DescriptionString().c_str(),
|
||||
(ContextCommand)((uint32_t)ContextCommand::FIRST_STYLE + s->h.v));
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!empty) AddContextMenuItem(NULL, ContextCommand::SEPARATOR);
|
||||
|
||||
AddContextMenuItem(_("No Style"), ContextCommand::NO_STYLE);
|
||||
AddContextMenuItem(_("Newly Created Custom Style..."), ContextCommand::NEW_CUSTOM_STYLE);
|
||||
}
|
||||
|
||||
void GraphicsWindow::MouseRightUp(double x, double y) {
|
||||
SS.extraLine.draw = false;
|
||||
InvalidateGraphics();
|
||||
@ -556,6 +538,7 @@ void GraphicsWindow::MouseRightUp(double x, double y) {
|
||||
v = v.Plus(projRight.ScaledBy(x/scale));
|
||||
v = v.Plus(projUp.ScaledBy(y/scale));
|
||||
|
||||
Platform::MenuRef menu = Platform::CreateMenu();
|
||||
context.active = true;
|
||||
|
||||
if(!hover.IsEmpty()) {
|
||||
@ -565,37 +548,91 @@ void GraphicsWindow::MouseRightUp(double x, double y) {
|
||||
GroupSelection();
|
||||
|
||||
bool itemsSelected = (gs.n > 0 || gs.constraints > 0);
|
||||
int addAfterPoint = -1;
|
||||
|
||||
if(itemsSelected) {
|
||||
if(gs.stylables > 0) {
|
||||
ContextMenuListStyles();
|
||||
AddContextMenuItem(_("Assign to Style"), ContextCommand::SUBMENU);
|
||||
Platform::MenuRef styleMenu = menu->AddSubMenu(_("Assign to Style"));
|
||||
|
||||
bool empty = true;
|
||||
for(const Style &s : SK.style) {
|
||||
if(s.h.v < Style::FIRST_CUSTOM) continue;
|
||||
|
||||
styleMenu->AddItem(s.DescriptionString(), [&]() {
|
||||
Style::AssignSelectionToStyle(s.h.v);
|
||||
});
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!empty) styleMenu->AddSeparator();
|
||||
|
||||
styleMenu->AddItem(_("No Style"), [&]() {
|
||||
Style::AssignSelectionToStyle(0);
|
||||
});
|
||||
styleMenu->AddItem(_("Newly Created Custom Style..."), [&]() {
|
||||
uint32_t vs = Style::CreateCustomStyle();
|
||||
Style::AssignSelectionToStyle(vs);
|
||||
ForceTextWindowShown();
|
||||
});
|
||||
}
|
||||
if(gs.n + gs.constraints == 1) {
|
||||
AddContextMenuItem(_("Group Info"), ContextCommand::GROUP_INFO);
|
||||
menu->AddItem(_("Group Info"), [&]() {
|
||||
hGroup hg;
|
||||
if(gs.entities == 1) {
|
||||
hg = SK.GetEntity(gs.entity[0])->group;
|
||||
} else if(gs.points == 1) {
|
||||
hg = SK.GetEntity(gs.point[0])->group;
|
||||
} else if(gs.constraints == 1) {
|
||||
hg = SK.GetConstraint(gs.constraint[0])->group;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
ClearSelection();
|
||||
|
||||
SS.TW.GoToScreen(TextWindow::Screen::GROUP_INFO);
|
||||
SS.TW.shown.group = hg;
|
||||
SS.ScheduleShowTW();
|
||||
ForceTextWindowShown();
|
||||
});
|
||||
}
|
||||
if(gs.n + gs.constraints == 1 && gs.stylables == 1) {
|
||||
AddContextMenuItem(_("Style Info"), ContextCommand::STYLE_INFO);
|
||||
menu->AddItem(_("Style Info"), [&]() {
|
||||
hStyle hs;
|
||||
if(gs.entities == 1) {
|
||||
hs = Style::ForEntity(gs.entity[0]);
|
||||
} else if(gs.points == 1) {
|
||||
hs = Style::ForEntity(gs.point[0]);
|
||||
} else if(gs.constraints == 1) {
|
||||
hs = SK.GetConstraint(gs.constraint[0])->GetStyle();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
ClearSelection();
|
||||
|
||||
SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO);
|
||||
SS.TW.shown.style = hs;
|
||||
SS.ScheduleShowTW();
|
||||
ForceTextWindowShown();
|
||||
});
|
||||
}
|
||||
if(gs.withEndpoints > 0) {
|
||||
AddContextMenuItem(_("Select Edge Chain"), ContextCommand::SELECT_CHAIN);
|
||||
menu->AddItem(_("Select Edge Chain"),
|
||||
[&]() { MenuEdit(Command::SELECT_CHAIN); });
|
||||
}
|
||||
if(gs.constraints == 1 && gs.n == 0) {
|
||||
Constraint *c = SK.GetConstraint(gs.constraint[0]);
|
||||
if(c->HasLabel() && c->type != Constraint::Type::COMMENT) {
|
||||
AddContextMenuItem(_("Toggle Reference Dimension"),
|
||||
ContextCommand::REFERENCE_DIM);
|
||||
menu->AddItem(_("Toggle Reference Dimension"),
|
||||
[&]() { Constraint::MenuConstrain(Command::REFERENCE); });
|
||||
}
|
||||
if(c->type == Constraint::Type::ANGLE ||
|
||||
c->type == Constraint::Type::EQUAL_ANGLE)
|
||||
{
|
||||
AddContextMenuItem(_("Other Supplementary Angle"),
|
||||
ContextCommand::OTHER_ANGLE);
|
||||
menu->AddItem(_("Other Supplementary Angle"),
|
||||
[&]() { Constraint::MenuConstrain(Command::OTHER_ANGLE); });
|
||||
}
|
||||
}
|
||||
if(gs.constraintLabels > 0 || gs.points > 0) {
|
||||
AddContextMenuItem(_("Snap to Grid"), ContextCommand::SNAP_TO_GRID);
|
||||
menu->AddItem(_("Snap to Grid"),
|
||||
[&]() { MenuEdit(Command::SNAP_TO_GRID); });
|
||||
}
|
||||
|
||||
if(gs.points == 1 && gs.point[0].isFromRequest()) {
|
||||
@ -603,22 +640,72 @@ void GraphicsWindow::MouseRightUp(double x, double y) {
|
||||
int index = r->IndexOfPoint(gs.point[0]);
|
||||
if((r->type == Request::Type::CUBIC && (index > 1 && index < r->extraPoints + 2)) ||
|
||||
r->type == Request::Type::CUBIC_PERIODIC) {
|
||||
AddContextMenuItem(_("Remove Spline Point"), ContextCommand::REMOVE_SPLINE_PT);
|
||||
menu->AddItem(_("Remove Spline Point"), [&]() {
|
||||
int index = r->IndexOfPoint(gs.point[0]);
|
||||
ssassert(r->extraPoints != 0,
|
||||
"Expected a bezier with interior control points");
|
||||
|
||||
SS.UndoRemember();
|
||||
Entity *e = SK.GetEntity(r->h.entity(0));
|
||||
|
||||
// First, fix point-coincident constraints involving this point.
|
||||
// Then, remove all other constraints, since they would otherwise
|
||||
// jump to an adjacent one and mess up the bezier after generation.
|
||||
FixConstraintsForPointBeingDeleted(e->point[index]);
|
||||
RemoveConstraintsForPointBeingDeleted(e->point[index]);
|
||||
|
||||
for(int i = index; i < MAX_POINTS_IN_ENTITY - 1; i++) {
|
||||
if(e->point[i + 1].v == 0) break;
|
||||
Entity *p0 = SK.GetEntity(e->point[i]);
|
||||
Entity *p1 = SK.GetEntity(e->point[i + 1]);
|
||||
ReplacePointInConstraints(p1->h, p0->h);
|
||||
p0->PointForceTo(p1->PointGetNum());
|
||||
}
|
||||
r->extraPoints--;
|
||||
SS.MarkGroupDirtyByEntity(gs.point[0]);
|
||||
ClearSelection();
|
||||
});
|
||||
}
|
||||
}
|
||||
if(gs.entities == 1 && gs.entity[0].isFromRequest()) {
|
||||
Request *r = SK.GetRequest(gs.entity[0].request());
|
||||
if(r->type == Request::Type::CUBIC || r->type == Request::Type::CUBIC_PERIODIC) {
|
||||
Entity *e = SK.GetEntity(gs.entity[0]);
|
||||
addAfterPoint = e->GetPositionOfPoint(GetCamera(), Point2d::From(x, y));
|
||||
int addAfterPoint = e->GetPositionOfPoint(GetCamera(), Point2d::From(x, y));
|
||||
ssassert(addAfterPoint != -1, "Expected a nearest bezier point to be located");
|
||||
// Skip derivative point.
|
||||
if(r->type == Request::Type::CUBIC) addAfterPoint++;
|
||||
AddContextMenuItem(_("Add Spline Point"), ContextCommand::ADD_SPLINE_PT);
|
||||
menu->AddItem(_("Add Spline Point"), [&]() {
|
||||
int pointCount = r->extraPoints +
|
||||
((r->type == Request::Type::CUBIC_PERIODIC) ? 3 : 4);
|
||||
if(pointCount >= MAX_POINTS_IN_ENTITY) {
|
||||
Error(_("Cannot add spline point: maximum number of points reached."));
|
||||
return;
|
||||
}
|
||||
|
||||
SS.UndoRemember();
|
||||
r->extraPoints++;
|
||||
SS.MarkGroupDirtyByEntity(gs.entity[0]);
|
||||
SS.GenerateAll(SolveSpaceUI::Generate::REGEN);
|
||||
|
||||
Entity *e = SK.GetEntity(r->h.entity(0));
|
||||
for(int i = MAX_POINTS_IN_ENTITY; i > addAfterPoint + 1; i--) {
|
||||
Entity *p0 = SK.entity.FindByIdNoOops(e->point[i]);
|
||||
if(p0 == NULL) continue;
|
||||
Entity *p1 = SK.GetEntity(e->point[i - 1]);
|
||||
ReplacePointInConstraints(p1->h, p0->h);
|
||||
p0->PointForceTo(p1->PointGetNum());
|
||||
}
|
||||
Entity *p = SK.GetEntity(e->point[addAfterPoint + 1]);
|
||||
p->PointForceTo(v);
|
||||
SS.MarkGroupDirtyByEntity(gs.entity[0]);
|
||||
ClearSelection();
|
||||
});
|
||||
}
|
||||
}
|
||||
if(gs.entities == gs.n) {
|
||||
AddContextMenuItem(_("Toggle Construction"), ContextCommand::CONSTRUCTION);
|
||||
menu->AddItem(_("Toggle Construction"),
|
||||
[&]() { MenuRequest(Command::CONSTRUCTION); });
|
||||
}
|
||||
|
||||
if(gs.points == 1) {
|
||||
@ -632,240 +719,68 @@ void GraphicsWindow::MouseRightUp(double x, double y) {
|
||||
}
|
||||
}
|
||||
if(c) {
|
||||
AddContextMenuItem(_("Delete Point-Coincident Constraint"),
|
||||
ContextCommand::DEL_COINCIDENT);
|
||||
menu->AddItem(_("Delete Point-Coincident Constraint"), [&]() {
|
||||
if(!p->IsPoint()) return;
|
||||
|
||||
SS.UndoRemember();
|
||||
SK.constraint.ClearTags();
|
||||
Constraint *c;
|
||||
for(c = SK.constraint.First(); c; c = SK.constraint.NextAfter(c)) {
|
||||
if(c->type != Constraint::Type::POINTS_COINCIDENT) continue;
|
||||
if(c->ptA.v == p->h.v || c->ptB.v == p->h.v) {
|
||||
c->tag = 1;
|
||||
}
|
||||
}
|
||||
SK.constraint.RemoveTagged();
|
||||
ClearSelection();
|
||||
});
|
||||
}
|
||||
}
|
||||
AddContextMenuItem(NULL, ContextCommand::SEPARATOR);
|
||||
menu->AddSeparator();
|
||||
if(LockedInWorkplane()) {
|
||||
AddContextMenuItem(_("Cut"), ContextCommand::CUT_SEL);
|
||||
AddContextMenuItem(_("Copy"), ContextCommand::COPY_SEL);
|
||||
menu->AddItem(_("Cut"),
|
||||
[&]() { MenuClipboard(Command::CUT); });
|
||||
menu->AddItem(_("Copy"),
|
||||
[&]() { MenuClipboard(Command::COPY); });
|
||||
}
|
||||
} else {
|
||||
AddContextMenuItem(_("Select All"), ContextCommand::SELECT_ALL);
|
||||
menu->AddItem(_("Select All"),
|
||||
[&]() { MenuEdit(Command::SELECT_ALL); });
|
||||
}
|
||||
|
||||
if((SS.clipboard.r.n > 0 || SS.clipboard.c.n > 0) && LockedInWorkplane()) {
|
||||
AddContextMenuItem(_("Paste"), ContextCommand::PASTE);
|
||||
AddContextMenuItem(_("Paste Transformed..."), ContextCommand::PASTE_XFRM);
|
||||
menu->AddItem(_("Paste"),
|
||||
[&]() { MenuClipboard(Command::PASTE); });
|
||||
menu->AddItem(_("Paste Transformed..."),
|
||||
[&]() { MenuClipboard(Command::PASTE_TRANSFORM); });
|
||||
}
|
||||
|
||||
if(itemsSelected) {
|
||||
AddContextMenuItem(_("Delete"), ContextCommand::DELETE_SEL);
|
||||
AddContextMenuItem(NULL, ContextCommand::SEPARATOR);
|
||||
AddContextMenuItem(_("Unselect All"), ContextCommand::UNSELECT_ALL);
|
||||
menu->AddItem(_("Delete"),
|
||||
[&]() { MenuClipboard(Command::DELETE); });
|
||||
menu->AddSeparator();
|
||||
menu->AddItem(_("Unselect All"),
|
||||
[&]() { MenuEdit(Command::UNSELECT_ALL); });
|
||||
}
|
||||
// If only one item is selected, then it must be the one that we just
|
||||
// selected from the hovered item; in which case unselect all and hovered
|
||||
// are equivalent.
|
||||
if(!hover.IsEmpty() && selection.n > 1) {
|
||||
AddContextMenuItem(_("Unselect Hovered"), ContextCommand::UNSELECT_HOVERED);
|
||||
}
|
||||
|
||||
if(itemsSelected) {
|
||||
AddContextMenuItem(NULL, ContextCommand::SEPARATOR);
|
||||
AddContextMenuItem(_("Zoom to Fit"), ContextCommand::ZOOM_TO_FIT);
|
||||
}
|
||||
|
||||
ContextCommand ret = ShowContextMenu();
|
||||
switch(ret) {
|
||||
case ContextCommand::CANCELLED:
|
||||
// otherwise it was cancelled, so do nothing
|
||||
contextMenuCancelTime = GetMilliseconds();
|
||||
break;
|
||||
|
||||
case ContextCommand::UNSELECT_ALL:
|
||||
MenuEdit(Command::UNSELECT_ALL);
|
||||
break;
|
||||
|
||||
case ContextCommand::UNSELECT_HOVERED:
|
||||
menu->AddItem(_("Unselect Hovered"), [&] {
|
||||
if(!hover.IsEmpty()) {
|
||||
MakeUnselected(&hover, /*coincidentPointTrick=*/true);
|
||||
}
|
||||
break;
|
||||
|
||||
case ContextCommand::SELECT_CHAIN:
|
||||
MenuEdit(Command::SELECT_CHAIN);
|
||||
break;
|
||||
|
||||
case ContextCommand::CUT_SEL:
|
||||
MenuClipboard(Command::CUT);
|
||||
break;
|
||||
|
||||
case ContextCommand::COPY_SEL:
|
||||
MenuClipboard(Command::COPY);
|
||||
break;
|
||||
|
||||
case ContextCommand::PASTE:
|
||||
MenuClipboard(Command::PASTE);
|
||||
break;
|
||||
|
||||
case ContextCommand::PASTE_XFRM:
|
||||
MenuClipboard(Command::PASTE_TRANSFORM);
|
||||
break;
|
||||
|
||||
case ContextCommand::DELETE_SEL:
|
||||
MenuClipboard(Command::DELETE);
|
||||
break;
|
||||
|
||||
case ContextCommand::REFERENCE_DIM:
|
||||
Constraint::MenuConstrain(Command::REFERENCE);
|
||||
break;
|
||||
|
||||
case ContextCommand::OTHER_ANGLE:
|
||||
Constraint::MenuConstrain(Command::OTHER_ANGLE);
|
||||
break;
|
||||
|
||||
case ContextCommand::DEL_COINCIDENT: {
|
||||
SS.UndoRemember();
|
||||
if(!gs.point[0].v) break;
|
||||
Entity *p = SK.GetEntity(gs.point[0]);
|
||||
if(!p->IsPoint()) break;
|
||||
|
||||
SK.constraint.ClearTags();
|
||||
Constraint *c;
|
||||
for(c = SK.constraint.First(); c; c = SK.constraint.NextAfter(c)) {
|
||||
if(c->type != Constraint::Type::POINTS_COINCIDENT) continue;
|
||||
if(c->ptA.v == p->h.v || c->ptB.v == p->h.v) {
|
||||
c->tag = 1;
|
||||
}
|
||||
}
|
||||
SK.constraint.RemoveTagged();
|
||||
ClearSelection();
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextCommand::SNAP_TO_GRID:
|
||||
MenuEdit(Command::SNAP_TO_GRID);
|
||||
break;
|
||||
|
||||
case ContextCommand::CONSTRUCTION:
|
||||
MenuRequest(Command::CONSTRUCTION);
|
||||
break;
|
||||
|
||||
case ContextCommand::ZOOM_TO_FIT:
|
||||
MenuView(Command::ZOOM_TO_FIT);
|
||||
break;
|
||||
|
||||
case ContextCommand::SELECT_ALL:
|
||||
MenuEdit(Command::SELECT_ALL);
|
||||
break;
|
||||
|
||||
case ContextCommand::REMOVE_SPLINE_PT: {
|
||||
hRequest hr = gs.point[0].request();
|
||||
Request *r = SK.GetRequest(hr);
|
||||
|
||||
int index = r->IndexOfPoint(gs.point[0]);
|
||||
ssassert(r->extraPoints != 0, "Expected a bezier with interior control points");
|
||||
|
||||
SS.UndoRemember();
|
||||
Entity *e = SK.GetEntity(r->h.entity(0));
|
||||
|
||||
// First, fix point-coincident constraints involving this point.
|
||||
// Then, remove all other constraints, since they would otherwise
|
||||
// jump to an adjacent one and mess up the bezier after generation.
|
||||
FixConstraintsForPointBeingDeleted(e->point[index]);
|
||||
RemoveConstraintsForPointBeingDeleted(e->point[index]);
|
||||
|
||||
for(int i = index; i < MAX_POINTS_IN_ENTITY - 1; i++) {
|
||||
if(e->point[i + 1].v == 0) break;
|
||||
Entity *p0 = SK.GetEntity(e->point[i]);
|
||||
Entity *p1 = SK.GetEntity(e->point[i + 1]);
|
||||
ReplacePointInConstraints(p1->h, p0->h);
|
||||
p0->PointForceTo(p1->PointGetNum());
|
||||
}
|
||||
r->extraPoints--;
|
||||
SS.MarkGroupDirtyByEntity(gs.point[0]);
|
||||
ClearSelection();
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextCommand::ADD_SPLINE_PT: {
|
||||
hRequest hr = gs.entity[0].request();
|
||||
Request *r = SK.GetRequest(hr);
|
||||
|
||||
int pointCount = r->extraPoints + ((r->type == Request::Type::CUBIC_PERIODIC) ? 3 : 4);
|
||||
if(pointCount < MAX_POINTS_IN_ENTITY) {
|
||||
SS.UndoRemember();
|
||||
r->extraPoints++;
|
||||
SS.MarkGroupDirtyByEntity(gs.entity[0]);
|
||||
SS.GenerateAll(SolveSpaceUI::Generate::REGEN);
|
||||
|
||||
Entity *e = SK.GetEntity(r->h.entity(0));
|
||||
for(int i = MAX_POINTS_IN_ENTITY; i > addAfterPoint + 1; i--) {
|
||||
Entity *p0 = SK.entity.FindByIdNoOops(e->point[i]);
|
||||
if(p0 == NULL) continue;
|
||||
Entity *p1 = SK.GetEntity(e->point[i - 1]);
|
||||
ReplacePointInConstraints(p1->h, p0->h);
|
||||
p0->PointForceTo(p1->PointGetNum());
|
||||
}
|
||||
Entity *p = SK.GetEntity(e->point[addAfterPoint + 1]);
|
||||
p->PointForceTo(v);
|
||||
SS.MarkGroupDirtyByEntity(gs.entity[0]);
|
||||
ClearSelection();
|
||||
} else {
|
||||
Error(_("Cannot add spline point: maximum number of points reached."));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextCommand::GROUP_INFO: {
|
||||
hGroup hg;
|
||||
if(gs.entities == 1) {
|
||||
hg = SK.GetEntity(gs.entity[0])->group;
|
||||
} else if(gs.points == 1) {
|
||||
hg = SK.GetEntity(gs.point[0])->group;
|
||||
} else if(gs.constraints == 1) {
|
||||
hg = SK.GetConstraint(gs.constraint[0])->group;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
ClearSelection();
|
||||
|
||||
SS.TW.GoToScreen(TextWindow::Screen::GROUP_INFO);
|
||||
SS.TW.shown.group = hg;
|
||||
SS.ScheduleShowTW();
|
||||
ForceTextWindowShown();
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextCommand::STYLE_INFO: {
|
||||
hStyle hs;
|
||||
if(gs.entities == 1) {
|
||||
hs = Style::ForEntity(gs.entity[0]);
|
||||
} else if(gs.points == 1) {
|
||||
hs = Style::ForEntity(gs.point[0]);
|
||||
} else if(gs.constraints == 1) {
|
||||
hs = SK.GetConstraint(gs.constraint[0])->GetStyle();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
ClearSelection();
|
||||
|
||||
SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO);
|
||||
SS.TW.shown.style = hs;
|
||||
SS.ScheduleShowTW();
|
||||
ForceTextWindowShown();
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextCommand::NEW_CUSTOM_STYLE: {
|
||||
uint32_t v = Style::CreateCustomStyle();
|
||||
Style::AssignSelectionToStyle(v);
|
||||
ForceTextWindowShown();
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextCommand::NO_STYLE:
|
||||
Style::AssignSelectionToStyle(0);
|
||||
break;
|
||||
|
||||
default:
|
||||
ssassert(ret >= ContextCommand::FIRST_STYLE, "Expected a style to be chosen");
|
||||
Style::AssignSelectionToStyle((uint32_t)ret - (uint32_t)ContextCommand::FIRST_STYLE);
|
||||
break;
|
||||
});
|
||||
}
|
||||
|
||||
if(itemsSelected) {
|
||||
menu->AddSeparator();
|
||||
menu->AddItem(_("Zoom to Fit"),
|
||||
[&]() { MenuView(Command::ZOOM_TO_FIT); });
|
||||
}
|
||||
|
||||
menu->PopUp();
|
||||
|
||||
context.active = false;
|
||||
SS.ScheduleShowTW();
|
||||
}
|
||||
@ -1344,15 +1259,7 @@ void GraphicsWindow::MouseLeftUp(double mx, double my) {
|
||||
break;
|
||||
|
||||
case Pending::NONE:
|
||||
// We need to clear the selection here, and not in the mouse down
|
||||
// event, since a mouse down without anything hovered could also
|
||||
// be the start of marquee selection. But don't do that on the
|
||||
// left click to cancel a context menu. The time delay is an ugly
|
||||
// hack.
|
||||
if(hover.IsEmpty() &&
|
||||
(contextMenuCancelTime == 0 ||
|
||||
(GetMilliseconds() - contextMenuCancelTime) > 200))
|
||||
{
|
||||
if(hover.IsEmpty()) {
|
||||
ClearSelection();
|
||||
}
|
||||
break;
|
||||
@ -1488,23 +1395,6 @@ void GraphicsWindow::EditControlDone(const char *s) {
|
||||
}
|
||||
}
|
||||
|
||||
bool GraphicsWindow::KeyDown(int c) {
|
||||
if(c == '\b') {
|
||||
// Treat backspace identically to escape.
|
||||
MenuEdit(Command::UNSELECT_ALL);
|
||||
return true;
|
||||
} else if(c == '=') {
|
||||
// Treat = as +. This is specific to US (and US-compatible) keyboard layouts,
|
||||
// but makes zooming from keyboard much more usable on these.
|
||||
// Ideally we'd have a platform-independent way of binding to a particular
|
||||
// physical key regardless of shift status...
|
||||
MenuView(Command::ZOOM_IN);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void GraphicsWindow::MouseScroll(double x, double y, int delta) {
|
||||
double offsetRight = offset.Dot(projRight);
|
||||
double offsetUp = offset.Dot(projUp);
|
||||
|
@ -301,25 +301,38 @@ CONVERT(Rect)
|
||||
SolveSpace::SS.GW.MouseLeave();
|
||||
}
|
||||
|
||||
- (void)keyDown:(NSEvent*)event {
|
||||
int chr = 0;
|
||||
if(NSString *nsChr = [event charactersIgnoringModifiers])
|
||||
- (void)keyDown:(NSEvent*)nsEvent {
|
||||
using SolveSpace::Platform::KeyboardEvent;
|
||||
|
||||
KeyboardEvent event = {};
|
||||
event.type = KeyboardEvent::Type::PRESS;
|
||||
|
||||
NSUInteger flags = [nsEvent modifierFlags];
|
||||
if(flags & NSShiftKeyMask)
|
||||
event.shiftDown = true;
|
||||
if(flags & NSCommandKeyMask)
|
||||
event.controlDown = true;
|
||||
if(flags & ~(NSShiftKeyMask|NSCommandKeyMask)) {
|
||||
[super keyDown:nsEvent];
|
||||
return;
|
||||
}
|
||||
|
||||
unichar chr = 0;
|
||||
if(NSString *nsChr = [nsEvent charactersIgnoringModifiers])
|
||||
chr = [nsChr characterAtIndex:0];
|
||||
|
||||
if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey)
|
||||
chr = SolveSpace::GraphicsWindow::FUNCTION_KEY_BASE + (chr - NSF1FunctionKey);
|
||||
if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) {
|
||||
event.key = KeyboardEvent::Key::FUNCTION;
|
||||
event.num = chr - NSF1FunctionKey + 1;
|
||||
} else {
|
||||
event.key = KeyboardEvent::Key::CHARACTER;
|
||||
event.chr = chr;
|
||||
}
|
||||
|
||||
NSUInteger flags = [event modifierFlags];
|
||||
if(flags & NSShiftKeyMask)
|
||||
chr |= SolveSpace::GraphicsWindow::SHIFT_MASK;
|
||||
if(flags & NSCommandKeyMask)
|
||||
chr |= SolveSpace::GraphicsWindow::CTRL_MASK;
|
||||
if(SolveSpace::SS.GW.KeyboardEvent(event))
|
||||
return;
|
||||
|
||||
// override builtin behavior: "focus on next cell", "close window"
|
||||
if(chr == '\t' || chr == '\x1b')
|
||||
[[NSApp mainMenu] performKeyEquivalent:event];
|
||||
else if(!chr || !SolveSpace::SS.GW.KeyDown(chr))
|
||||
[super keyDown:event];
|
||||
[super keyDown:nsEvent];
|
||||
}
|
||||
|
||||
- (void)startEditing:(NSString*)text at:(NSPoint)xy withHeight:(double)fontHeight
|
||||
@ -462,238 +475,6 @@ bool GraphicsEditControlIsVisible() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
static SolveSpace::ContextCommand contextMenuChoice;
|
||||
|
||||
@interface ContextMenuResponder : NSObject
|
||||
+ (void)handleClick:(id)sender;
|
||||
@end
|
||||
|
||||
@implementation ContextMenuResponder
|
||||
+ (void)handleClick:(id)sender {
|
||||
contextMenuChoice = (SolveSpace::ContextCommand)[sender tag];
|
||||
}
|
||||
@end
|
||||
|
||||
namespace SolveSpace {
|
||||
NSMenu *contextMenu, *contextSubmenu;
|
||||
|
||||
void AddContextMenuItem(const char *label, ContextCommand cmd) {
|
||||
NSMenuItem *menuItem;
|
||||
if(label) {
|
||||
menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label]
|
||||
action:@selector(handleClick:) keyEquivalent:@""];
|
||||
[menuItem setTarget:[ContextMenuResponder class]];
|
||||
[menuItem setTag:(NSInteger)cmd];
|
||||
} else {
|
||||
menuItem = [NSMenuItem separatorItem];
|
||||
}
|
||||
|
||||
if(cmd == SolveSpace::ContextCommand::SUBMENU) {
|
||||
[menuItem setSubmenu:contextSubmenu];
|
||||
contextSubmenu = nil;
|
||||
}
|
||||
|
||||
if(contextSubmenu) {
|
||||
[contextSubmenu addItem:menuItem];
|
||||
} else {
|
||||
if(!contextMenu) {
|
||||
contextMenu = [[NSMenu alloc]
|
||||
initWithTitle:[NSString stringWithUTF8String:label]];
|
||||
}
|
||||
|
||||
[contextMenu addItem:menuItem];
|
||||
}
|
||||
}
|
||||
|
||||
void CreateContextSubmenu() {
|
||||
ssassert(!contextSubmenu, "Unexpected nested submenu");
|
||||
|
||||
contextSubmenu = [[NSMenu alloc] initWithTitle:@""];
|
||||
}
|
||||
|
||||
ContextCommand ShowContextMenu() {
|
||||
if(!contextMenu)
|
||||
return ContextCommand::CANCELLED;
|
||||
|
||||
[NSMenu popUpContextMenu:contextMenu
|
||||
withEvent:[GWView lastContextMenuEvent] forView:GWView];
|
||||
|
||||
contextMenu = nil;
|
||||
|
||||
return contextMenuChoice;
|
||||
}
|
||||
};
|
||||
|
||||
/* Main menu */
|
||||
|
||||
@interface MainMenuResponder : NSObject
|
||||
+ (void)handleStatic:(id)sender;
|
||||
+ (void)handleRecent:(id)sender;
|
||||
@end
|
||||
|
||||
@implementation MainMenuResponder
|
||||
+ (void)handleStatic:(id)sender {
|
||||
SolveSpace::GraphicsWindow::MenuEntry *entry =
|
||||
(SolveSpace::GraphicsWindow::MenuEntry*)[sender tag];
|
||||
|
||||
if(entry->fn && ![(NSMenuItem*)sender hasSubmenu])
|
||||
entry->fn(entry->id);
|
||||
}
|
||||
|
||||
+ (void)handleRecent:(id)sender {
|
||||
uint32_t cmd = [sender tag];
|
||||
if(cmd >= (uint32_t)SolveSpace::Command::RECENT_OPEN &&
|
||||
cmd < ((uint32_t)SolveSpace::Command::RECENT_OPEN + SolveSpace::MAX_RECENT)) {
|
||||
SolveSpace::SolveSpaceUI::MenuFile((SolveSpace::Command)cmd);
|
||||
} else if(cmd >= (uint32_t)SolveSpace::Command::RECENT_LINK &&
|
||||
cmd < ((uint32_t)SolveSpace::Command::RECENT_LINK + SolveSpace::MAX_RECENT)) {
|
||||
SolveSpace::Group::MenuGroup((SolveSpace::Command)cmd);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)handleLocale:(id)sender {
|
||||
uint32_t offset = [sender tag];
|
||||
SolveSpace::SolveSpaceUI::MenuHelp(
|
||||
(SolveSpace::Command)((uint32_t)SolveSpace::Command::LOCALE + offset));
|
||||
}
|
||||
@end
|
||||
|
||||
namespace SolveSpace {
|
||||
std::map<uint32_t, NSMenuItem*> mainMenuItems;
|
||||
|
||||
void InitMainMenu(NSMenu *mainMenu) {
|
||||
NSMenuItem *menuItem = NULL;
|
||||
NSMenu *levels[5] = {mainMenu, 0};
|
||||
NSString *label;
|
||||
|
||||
while([mainMenu numberOfItems] != 1) {
|
||||
[mainMenu removeItemAtIndex:1];
|
||||
}
|
||||
|
||||
const GraphicsWindow::MenuEntry *entry = &GraphicsWindow::menu[0];
|
||||
int current_level = 0;
|
||||
while(entry->level >= 0) {
|
||||
if(entry->level > current_level) {
|
||||
NSMenu *menu = [[NSMenu alloc] initWithTitle:label];
|
||||
[menu setAutoenablesItems:NO];
|
||||
[menuItem setSubmenu:menu];
|
||||
|
||||
ssassert((unsigned)entry->level < sizeof(levels) / sizeof(levels[0]),
|
||||
"Unexpected depth of menu nesting");
|
||||
|
||||
levels[entry->level] = menu;
|
||||
}
|
||||
|
||||
current_level = entry->level;
|
||||
|
||||
if(entry->label) {
|
||||
label = Wrap(Translate(entry->label));
|
||||
/* OS X does not support mnemonics */
|
||||
label = [label stringByReplacingOccurrencesOfString:@"&" withString:@""];
|
||||
|
||||
unichar accelChar = entry->accel &
|
||||
~(GraphicsWindow::SHIFT_MASK | GraphicsWindow::CTRL_MASK);
|
||||
if(accelChar > GraphicsWindow::FUNCTION_KEY_BASE &&
|
||||
accelChar <= GraphicsWindow::FUNCTION_KEY_BASE + 12) {
|
||||
accelChar = NSF1FunctionKey + (accelChar - GraphicsWindow::FUNCTION_KEY_BASE - 1);
|
||||
} else if(accelChar == GraphicsWindow::DELETE_KEY) {
|
||||
accelChar = NSBackspaceCharacter;
|
||||
}
|
||||
NSString *accel = [NSString stringWithCharacters:&accelChar length:1];
|
||||
|
||||
menuItem = [levels[entry->level] addItemWithTitle:label
|
||||
action:NULL keyEquivalent:[accel lowercaseString]];
|
||||
|
||||
NSUInteger modifierMask = 0;
|
||||
if(entry->accel & GraphicsWindow::SHIFT_MASK)
|
||||
modifierMask |= NSShiftKeyMask;
|
||||
else if(entry->accel & GraphicsWindow::CTRL_MASK)
|
||||
modifierMask |= NSCommandKeyMask;
|
||||
[menuItem setKeyEquivalentModifierMask:modifierMask];
|
||||
|
||||
[menuItem setTag:(NSInteger)entry];
|
||||
[menuItem setTarget:[MainMenuResponder class]];
|
||||
[menuItem setAction:@selector(handleStatic:)];
|
||||
} else {
|
||||
[levels[entry->level] addItem:[NSMenuItem separatorItem]];
|
||||
}
|
||||
|
||||
if(entry->id == Command::LOCALE) {
|
||||
NSMenu *localeMenu = [[NSMenu alloc] initWithTitle:label];
|
||||
[menuItem setSubmenu:localeMenu];
|
||||
|
||||
size_t i = 0;
|
||||
for(auto locale : Locales()) {
|
||||
NSMenuItem *localeMenuItem =
|
||||
[localeMenu addItemWithTitle:
|
||||
Wrap(locale.displayName)
|
||||
action:NULL keyEquivalent:@""];
|
||||
[localeMenuItem setTag:(NSInteger)i++];
|
||||
[localeMenuItem setTarget:[MainMenuResponder class]];
|
||||
[localeMenuItem setAction:@selector(handleLocale:)];
|
||||
}
|
||||
}
|
||||
|
||||
mainMenuItems[(uint32_t)entry->id] = menuItem;
|
||||
|
||||
++entry;
|
||||
}
|
||||
}
|
||||
|
||||
void EnableMenuByCmd(SolveSpace::Command cmd, bool enabled) {
|
||||
[mainMenuItems[(uint32_t)cmd] setEnabled:enabled];
|
||||
}
|
||||
|
||||
void CheckMenuByCmd(SolveSpace::Command cmd, bool checked) {
|
||||
[mainMenuItems[(uint32_t)cmd] setState:(checked ? NSOnState : NSOffState)];
|
||||
}
|
||||
|
||||
void RadioMenuByCmd(SolveSpace::Command cmd, bool selected) {
|
||||
CheckMenuByCmd(cmd, selected);
|
||||
}
|
||||
|
||||
static void RefreshRecentMenu(SolveSpace::Command cmd, SolveSpace::Command base) {
|
||||
NSMenuItem *recent = mainMenuItems[(uint32_t)cmd];
|
||||
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
|
||||
[recent setSubmenu:menu];
|
||||
|
||||
if(RecentFile[0].IsEmpty()) {
|
||||
NSMenuItem *placeholder = [[NSMenuItem alloc]
|
||||
initWithTitle:Wrap(_("(no recent files)")) action:nil keyEquivalent:@""];
|
||||
[placeholder setEnabled:NO];
|
||||
[menu addItem:placeholder];
|
||||
} else {
|
||||
for(size_t i = 0; i < MAX_RECENT; i++) {
|
||||
if(RecentFile[i].IsEmpty()) break;
|
||||
|
||||
NSMenuItem *item = [[NSMenuItem alloc]
|
||||
initWithTitle:[Wrap(RecentFile[i].raw)
|
||||
stringByAbbreviatingWithTildeInPath]
|
||||
action:nil keyEquivalent:@""];
|
||||
[item setTag:((uint32_t)base + i)];
|
||||
[item setAction:@selector(handleRecent:)];
|
||||
[item setTarget:[MainMenuResponder class]];
|
||||
[menu addItem:item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RefreshRecentMenus() {
|
||||
RefreshRecentMenu(Command::OPEN_RECENT, Command::RECENT_OPEN);
|
||||
RefreshRecentMenu(Command::GROUP_RECENT, Command::RECENT_LINK);
|
||||
}
|
||||
|
||||
void ToggleMenuBar() {
|
||||
[NSMenu setMenuBarVisible:![NSMenu menuBarVisible]];
|
||||
}
|
||||
|
||||
bool MenuBarIsVisible() {
|
||||
return [NSMenu menuBarVisible];
|
||||
}
|
||||
}
|
||||
|
||||
/* Save/load */
|
||||
|
||||
bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension,
|
||||
@ -1157,11 +938,8 @@ std::vector<SolveSpace::Platform::Path> SolveSpace::GetFontFiles() {
|
||||
}
|
||||
@end
|
||||
|
||||
void SolveSpace::RefreshLocale() {
|
||||
void SolveSpace::SetMainMenu(Platform::MenuBarRef menuBar) {
|
||||
SS.UpdateWindowTitle();
|
||||
SolveSpace::InitMainMenu([NSApp mainMenu]);
|
||||
RefreshRecentMenus();
|
||||
|
||||
[TW setTitle:Wrap(C_("title", "Property Browser"))];
|
||||
}
|
||||
|
||||
|
@ -272,7 +272,7 @@ protected:
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
return Gtk::Fixed::on_key_press_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,7 +281,7 @@ protected:
|
||||
_entry.event((GdkEvent *)event);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
return Gtk::Fixed::on_key_release_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,8 +433,7 @@ public:
|
||||
GraphicsWindowGtk() : _overlay(_widget), _is_fullscreen(false) {
|
||||
set_default_size(900, 600);
|
||||
|
||||
_box.pack_start(_menubar, false, true);
|
||||
_box.pack_start(_overlay, true, true);
|
||||
_box.pack_end(_overlay, true, true);
|
||||
|
||||
add(_box);
|
||||
|
||||
@ -450,7 +449,17 @@ public:
|
||||
return _overlay;
|
||||
}
|
||||
|
||||
Gtk::MenuBar &get_menubar() {
|
||||
void set_menubar(Gtk::MenuBar *menubar) {
|
||||
if(_menubar)
|
||||
_box.remove(*_menubar);
|
||||
_menubar = menubar;
|
||||
if(_menubar) {
|
||||
_menubar->show_all();
|
||||
_box.pack_start(*_menubar, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
Gtk::MenuBar *get_menubar() {
|
||||
return _menubar;
|
||||
}
|
||||
|
||||
@ -489,57 +498,34 @@ protected:
|
||||
return Gtk::Window::on_window_state_event(event);
|
||||
}
|
||||
|
||||
bool on_key_press_event(GdkEventKey *event) override {
|
||||
int chr;
|
||||
bool on_key_press_event(GdkEventKey *gdk_event) override {
|
||||
Platform::KeyboardEvent event = {};
|
||||
event.type = Platform::KeyboardEvent::Type::PRESS;
|
||||
|
||||
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;
|
||||
|
||||
case GDK_KEY_KP_Decimal:
|
||||
chr = '.';
|
||||
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(gdk_event->state & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
|
||||
return Gtk::Window::on_key_press_event(gdk_event);
|
||||
}
|
||||
|
||||
if(event->state & GDK_SHIFT_MASK){
|
||||
chr |= GraphicsWindow::SHIFT_MASK;
|
||||
}
|
||||
if(event->state & GDK_CONTROL_MASK) {
|
||||
chr |= GraphicsWindow::CTRL_MASK;
|
||||
event.shiftDown = (gdk_event->state & GDK_SHIFT_MASK) != 0;
|
||||
event.controlDown = (gdk_event->state & GDK_CONTROL_MASK) != 0;
|
||||
|
||||
char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(gdk_event->keyval));
|
||||
if(chr != 0) {
|
||||
event.key = Platform::KeyboardEvent::Key::CHARACTER;
|
||||
event.chr = chr;
|
||||
} else if(gdk_event->keyval >= GDK_KEY_F1 &&
|
||||
gdk_event->keyval <= GDK_KEY_F12) {
|
||||
event.key = Platform::KeyboardEvent::Key::FUNCTION;
|
||||
event.num = gdk_event->keyval - GDK_KEY_F1 + 1;
|
||||
} else {
|
||||
return Gtk::Window::on_key_press_event(gdk_event);
|
||||
}
|
||||
|
||||
if(chr && SS.GW.KeyDown(chr)) {
|
||||
if(SS.GW.KeyboardEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(chr == '\t') {
|
||||
// Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=123994.
|
||||
GraphicsWindow::MenuView(Command::SHOW_TEXT_WND);
|
||||
return true;
|
||||
}
|
||||
|
||||
return Gtk::Window::on_key_press_event(event);
|
||||
return Gtk::Window::on_key_press_event(gdk_event);
|
||||
}
|
||||
|
||||
void on_editing_done(Glib::ustring value) {
|
||||
@ -549,7 +535,7 @@ protected:
|
||||
private:
|
||||
GraphicsWidget _widget;
|
||||
EditorOverlay _overlay;
|
||||
Gtk::MenuBar _menubar;
|
||||
Gtk::MenuBar *_menubar;
|
||||
Gtk::VBox _box;
|
||||
|
||||
bool _is_fullscreen;
|
||||
@ -609,297 +595,6 @@ bool GraphicsEditControlIsVisible(void) {
|
||||
return GW->get_overlay().is_editing();
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
class ContextMenuItem : public Gtk::MenuItem {
|
||||
public:
|
||||
static ContextCommand choice;
|
||||
|
||||
ContextMenuItem(const Glib::ustring &label, ContextCommand cmd, bool mnemonic=false) :
|
||||
Gtk::MenuItem(label, mnemonic), _cmd(cmd) {
|
||||
}
|
||||
|
||||
protected:
|
||||
void on_activate() override {
|
||||
Gtk::MenuItem::on_activate();
|
||||
|
||||
if(has_submenu())
|
||||
return;
|
||||
|
||||
choice = _cmd;
|
||||
}
|
||||
|
||||
/* 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.
|
||||
*/
|
||||
bool on_button_press_event(GdkEventButton *event) override {
|
||||
if(event->button == 1 && event->type == GDK_BUTTON_PRESS) {
|
||||
on_activate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return Gtk::MenuItem::on_button_press_event(event);
|
||||
}
|
||||
|
||||
private:
|
||||
ContextCommand _cmd;
|
||||
};
|
||||
|
||||
ContextCommand ContextMenuItem::choice = ContextCommand::CANCELLED;
|
||||
|
||||
static Gtk::Menu *context_menu = NULL, *context_submenu = NULL;
|
||||
|
||||
void AddContextMenuItem(const char *label, ContextCommand cmd) {
|
||||
Gtk::MenuItem *menu_item;
|
||||
if(label)
|
||||
menu_item = new ContextMenuItem(label, cmd);
|
||||
else
|
||||
menu_item = new Gtk::SeparatorMenuItem();
|
||||
|
||||
if(cmd == ContextCommand::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) {
|
||||
ssassert(!context_submenu, "Unexpected nested submenu");
|
||||
|
||||
context_submenu = new Gtk::Menu;
|
||||
}
|
||||
|
||||
ContextCommand ShowContextMenu(void) {
|
||||
if(!context_menu)
|
||||
return ContextCommand::CANCELLED;
|
||||
|
||||
Glib::RefPtr<Glib::MainLoop> loop = Glib::MainLoop::create();
|
||||
context_menu->signal_deactivate().
|
||||
connect(sigc::mem_fun(loop.operator->(), &Glib::MainLoop::quit));
|
||||
|
||||
ContextMenuItem::choice = ContextCommand::CANCELLED;
|
||||
|
||||
context_menu->show_all();
|
||||
context_menu->popup(3, GDK_CURRENT_TIME);
|
||||
|
||||
loop->run();
|
||||
|
||||
delete context_menu;
|
||||
context_menu = NULL;
|
||||
|
||||
return ContextMenuItem::choice;
|
||||
}
|
||||
|
||||
/* Main menu */
|
||||
|
||||
template<class MenuItem> class MainMenuItem : public MenuItem {
|
||||
public:
|
||||
MainMenuItem(const GraphicsWindow::MenuEntry &entry) :
|
||||
MenuItem(), _entry(entry), _synthetic(false) {
|
||||
Glib::ustring label(_entry.label);
|
||||
for(size_t 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) {
|
||||
case GraphicsWindow::DELETE_KEY:
|
||||
accel_key = GDK_KEY_Delete;
|
||||
break;
|
||||
|
||||
case GraphicsWindow::ESCAPE_KEY:
|
||||
accel_key = GDK_KEY_Escape;
|
||||
break;
|
||||
|
||||
case '\t':
|
||||
accel_key = GDK_KEY_Tab;
|
||||
break;
|
||||
|
||||
default:
|
||||
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);
|
||||
else
|
||||
accel_key = gdk_unicode_to_keyval(accel_key);
|
||||
|
||||
if(_entry.accel & GraphicsWindow::SHIFT_MASK)
|
||||
accel_mods |= Gdk::SHIFT_MASK;
|
||||
if(_entry.accel & GraphicsWindow::CTRL_MASK)
|
||||
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:
|
||||
void on_activate() override {
|
||||
MenuItem::on_activate();
|
||||
|
||||
if(_synthetic)
|
||||
_synthetic = false;
|
||||
else if(!MenuItem::has_submenu() && _entry.fn)
|
||||
_entry.fn(_entry.id);
|
||||
}
|
||||
|
||||
private:
|
||||
const GraphicsWindow::MenuEntry _entry;
|
||||
bool _synthetic;
|
||||
};
|
||||
|
||||
static std::map<uint32_t, 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);
|
||||
|
||||
ssassert((unsigned)entry->level < sizeof(levels) / sizeof(levels[0]),
|
||||
"Unexpected depth of menu nesting");
|
||||
|
||||
levels[entry->level] = menu;
|
||||
}
|
||||
|
||||
current_level = entry->level;
|
||||
|
||||
if(entry->label) {
|
||||
GraphicsWindow::MenuEntry localizedEntry = *entry;
|
||||
localizedEntry.label = Translate(entry->label).c_str();
|
||||
|
||||
switch(entry->kind) {
|
||||
case GraphicsWindow::MenuKind::NORMAL:
|
||||
menu_item = new MainMenuItem<Gtk::MenuItem>(localizedEntry);
|
||||
break;
|
||||
|
||||
case GraphicsWindow::MenuKind::CHECK:
|
||||
menu_item = new MainMenuItem<Gtk::CheckMenuItem>(localizedEntry);
|
||||
break;
|
||||
|
||||
case GraphicsWindow::MenuKind::RADIO:
|
||||
MainMenuItem<Gtk::CheckMenuItem> *radio_item =
|
||||
new MainMenuItem<Gtk::CheckMenuItem>(localizedEntry);
|
||||
radio_item->set_draw_as_radio(true);
|
||||
menu_item = radio_item;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
menu_item = new Gtk::SeparatorMenuItem();
|
||||
}
|
||||
|
||||
if(entry->id == Command::LOCALE) {
|
||||
Gtk::Menu *menu = new Gtk::Menu;
|
||||
menu_item->set_submenu(*menu);
|
||||
|
||||
size_t i = 0;
|
||||
for(auto locale : Locales()) {
|
||||
GraphicsWindow::MenuEntry localeEntry = {};
|
||||
localeEntry.label = locale.displayName.c_str();
|
||||
localeEntry.id = (Command)((uint32_t)Command::LOCALE + i++);
|
||||
localeEntry.fn = entry->fn;
|
||||
menu->append(*new MainMenuItem<Gtk::MenuItem>(localeEntry));
|
||||
}
|
||||
}
|
||||
|
||||
levels[entry->level]->append(*menu_item);
|
||||
|
||||
main_menu_items[(uint32_t)entry->id] = menu_item;
|
||||
|
||||
++entry;
|
||||
}
|
||||
}
|
||||
|
||||
void EnableMenuByCmd(Command cmd, bool enabled) {
|
||||
main_menu_items[(uint32_t)cmd]->set_sensitive(enabled);
|
||||
}
|
||||
|
||||
void CheckMenuByCmd(Command cmd, bool checked) {
|
||||
((MainMenuItem<Gtk::CheckMenuItem>*)main_menu_items[(uint32_t)cmd])->set_active(checked);
|
||||
}
|
||||
|
||||
void RadioMenuByCmd(Command cmd, bool selected) {
|
||||
SolveSpace::CheckMenuByCmd(cmd, selected);
|
||||
}
|
||||
|
||||
class RecentMenuItem : public Gtk::MenuItem {
|
||||
public:
|
||||
RecentMenuItem(const Glib::ustring& label, uint32_t cmd) :
|
||||
MenuItem(label), _cmd(cmd) {
|
||||
}
|
||||
|
||||
protected:
|
||||
void on_activate() override {
|
||||
if(_cmd >= (uint32_t)Command::RECENT_OPEN &&
|
||||
_cmd < ((uint32_t)Command::RECENT_OPEN + MAX_RECENT)) {
|
||||
SolveSpaceUI::MenuFile((Command)_cmd);
|
||||
} else if(_cmd >= (uint32_t)Command::RECENT_LINK &&
|
||||
_cmd < ((uint32_t)Command::RECENT_LINK + MAX_RECENT)) {
|
||||
Group::MenuGroup((Command)_cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t _cmd;
|
||||
};
|
||||
|
||||
static void RefreshRecentMenu(Command cmd, Command base) {
|
||||
Gtk::MenuItem *recent = static_cast<Gtk::MenuItem*>(main_menu_items[(uint32_t)cmd]);
|
||||
recent->unset_submenu();
|
||||
|
||||
Gtk::Menu *menu = new Gtk::Menu;
|
||||
recent->set_submenu(*menu);
|
||||
|
||||
if(RecentFile[0].IsEmpty()) {
|
||||
Gtk::MenuItem *placeholder = new Gtk::MenuItem(_("(no recent files)"));
|
||||
placeholder->set_sensitive(false);
|
||||
menu->append(*placeholder);
|
||||
} else {
|
||||
for(size_t i = 0; i < MAX_RECENT; i++) {
|
||||
if(RecentFile[i].IsEmpty())
|
||||
break;
|
||||
|
||||
RecentMenuItem *item = new RecentMenuItem(RecentFile[i].raw, (uint32_t)base + i);
|
||||
menu->append(*item);
|
||||
}
|
||||
}
|
||||
|
||||
menu->show_all();
|
||||
}
|
||||
|
||||
void RefreshRecentMenus(void) {
|
||||
RefreshRecentMenu(Command::OPEN_RECENT, Command::RECENT_OPEN);
|
||||
RefreshRecentMenu(Command::GROUP_RECENT, Command::RECENT_LINK);
|
||||
}
|
||||
|
||||
/* Save/load */
|
||||
|
||||
static std::string ConvertFilters(std::string active, const FileFilter ssFilters[],
|
||||
@ -1338,17 +1033,14 @@ static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *, gpointer)
|
||||
|
||||
/* Application lifecycle */
|
||||
|
||||
void RefreshLocale() {
|
||||
SS.UpdateWindowTitle();
|
||||
for(auto menu : GW->get_menubar().get_children()) {
|
||||
GW->get_menubar().remove(*menu);
|
||||
}
|
||||
InitMainMenu(&GW->get_menubar());
|
||||
RefreshRecentMenus();
|
||||
GW->get_menubar().show_all();
|
||||
GW->get_menubar().accelerate(*GW);
|
||||
GW->get_menubar().accelerate(*TW);
|
||||
void SetMainMenu(Platform::MenuBarRef menuBar) {
|
||||
static Platform::MenuBarRef _menuBar;
|
||||
GW->set_menubar((Gtk::MenuBar*)menuBar->NativePtr());
|
||||
GW->get_menubar()->accelerate(*GW);
|
||||
GW->get_menubar()->accelerate(*TW);
|
||||
_menuBar = menuBar;
|
||||
|
||||
SS.UpdateWindowTitle();
|
||||
TW->set_title(Title(C_("title", "Property Browser")));
|
||||
}
|
||||
|
||||
|
49
src/platform/gui.cpp
Normal file
49
src/platform/gui.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Platform-dependent GUI functionality that has only minor differences.
|
||||
//
|
||||
// Copyright 2018 whitequark
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "solvespace.h"
|
||||
|
||||
namespace SolveSpace {
|
||||
namespace Platform {
|
||||
|
||||
std::string AcceleratorDescription(const KeyboardEvent &accel) {
|
||||
std::string label;
|
||||
if(accel.controlDown) {
|
||||
#ifdef __APPLE__
|
||||
label += "⌘+";
|
||||
#else
|
||||
label += "Ctrl+";
|
||||
#endif
|
||||
}
|
||||
|
||||
if(accel.shiftDown) {
|
||||
label += "Shift+";
|
||||
}
|
||||
|
||||
switch(accel.key) {
|
||||
case KeyboardEvent::Key::FUNCTION:
|
||||
label += ssprintf("F%d", accel.num);
|
||||
break;
|
||||
|
||||
case KeyboardEvent::Key::CHARACTER:
|
||||
if(accel.chr == '\t') {
|
||||
label += "Tab";
|
||||
} else if(accel.chr == ' ') {
|
||||
label += "Space";
|
||||
} else if(accel.chr == '\x1b') {
|
||||
label += "Esc";
|
||||
} else if(accel.chr == '\x7f') {
|
||||
label += "Del";
|
||||
} else if(accel.chr != 0) {
|
||||
label += toupper((char)(accel.chr & 0xff));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -9,6 +9,39 @@
|
||||
|
||||
namespace Platform {
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Events
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// A keyboard input event.
|
||||
struct KeyboardEvent {
|
||||
enum class Type {
|
||||
PRESS,
|
||||
RELEASE,
|
||||
};
|
||||
|
||||
enum class Key {
|
||||
CHARACTER,
|
||||
FUNCTION,
|
||||
};
|
||||
|
||||
Type type;
|
||||
Key key;
|
||||
union {
|
||||
char32_t chr; // for Key::CHARACTER
|
||||
int num; // for Key::FUNCTION
|
||||
};
|
||||
bool shiftDown;
|
||||
bool controlDown;
|
||||
|
||||
bool Equals(const KeyboardEvent &other) {
|
||||
return type == other.type && key == other.key && num == other.num &&
|
||||
shiftDown == other.shiftDown && controlDown == other.controlDown;
|
||||
}
|
||||
};
|
||||
|
||||
std::string AcceleratorDescription(const KeyboardEvent &accel);
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Interfaces
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -27,6 +60,60 @@ typedef std::unique_ptr<Timer> TimerRef;
|
||||
|
||||
TimerRef CreateTimer();
|
||||
|
||||
// A native menu item.
|
||||
class MenuItem {
|
||||
public:
|
||||
enum class Indicator {
|
||||
NONE,
|
||||
CHECK_MARK,
|
||||
RADIO_MARK,
|
||||
};
|
||||
|
||||
std::function<void()> onTrigger;
|
||||
|
||||
virtual ~MenuItem() {}
|
||||
|
||||
virtual void SetAccelerator(KeyboardEvent accel) = 0;
|
||||
virtual void SetIndicator(Indicator type) = 0;
|
||||
virtual void SetEnabled(bool enabled) = 0;
|
||||
virtual void SetActive(bool active) = 0;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<MenuItem> MenuItemRef;
|
||||
|
||||
// A native menu.
|
||||
class Menu {
|
||||
public:
|
||||
virtual ~Menu() {}
|
||||
|
||||
virtual std::shared_ptr<MenuItem> AddItem(
|
||||
const std::string &label, std::function<void()> onTrigger = std::function<void()>()) = 0;
|
||||
virtual std::shared_ptr<Menu> AddSubMenu(const std::string &label) = 0;
|
||||
virtual void AddSeparator() = 0;
|
||||
|
||||
virtual void PopUp() = 0;
|
||||
|
||||
virtual void Clear() = 0;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<Menu> MenuRef;
|
||||
|
||||
// A native menu bar.
|
||||
class MenuBar {
|
||||
public:
|
||||
virtual ~MenuBar() {}
|
||||
|
||||
virtual std::shared_ptr<Menu> AddSubMenu(const std::string &label) = 0;
|
||||
|
||||
virtual void Clear() = 0;
|
||||
virtual void *NativePtr() = 0;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<MenuBar> MenuBarRef;
|
||||
|
||||
MenuRef CreateMenu();
|
||||
MenuBarRef GetOrCreateMainMenu(bool *unique);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -4,6 +4,10 @@
|
||||
// Copyright 2018 whitequark
|
||||
//-----------------------------------------------------------------------------
|
||||
#include <glibmm/main.h>
|
||||
#include <gtkmm/checkmenuitem.h>
|
||||
#include <gtkmm/separatormenuitem.h>
|
||||
#include <gtkmm/menu.h>
|
||||
#include <gtkmm/menubar.h>
|
||||
#include "solvespace.h"
|
||||
|
||||
namespace SolveSpace {
|
||||
@ -36,5 +40,224 @@ TimerRef CreateTimer() {
|
||||
return std::unique_ptr<TimerImplGtk>(new TimerImplGtk);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// GTK menu extensions
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
class GtkMenuItem : public Gtk::CheckMenuItem {
|
||||
Platform::MenuItem *_receiver;
|
||||
bool _has_indicator;
|
||||
bool _synthetic_event;
|
||||
|
||||
public:
|
||||
GtkMenuItem(Platform::MenuItem *receiver) :
|
||||
_receiver(receiver), _has_indicator(false), _synthetic_event(false) {
|
||||
}
|
||||
|
||||
void set_accel_key(const Gtk::AccelKey &accel_key) {
|
||||
Gtk::CheckMenuItem::set_accel_key(accel_key);
|
||||
}
|
||||
|
||||
bool has_indicator() const {
|
||||
return _has_indicator;
|
||||
}
|
||||
|
||||
void set_has_indicator(bool has_indicator) {
|
||||
_has_indicator = has_indicator;
|
||||
}
|
||||
|
||||
void set_active(bool active) {
|
||||
if(Gtk::CheckMenuItem::get_active() == active)
|
||||
return;
|
||||
|
||||
_synthetic_event = true;
|
||||
Gtk::CheckMenuItem::set_active(active);
|
||||
_synthetic_event = false;
|
||||
}
|
||||
|
||||
protected:
|
||||
void on_activate() override {
|
||||
Gtk::CheckMenuItem::on_activate();
|
||||
|
||||
if(!_synthetic_event && _receiver->onTrigger) {
|
||||
_receiver->onTrigger();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_indicator_vfunc(const Cairo::RefPtr<Cairo::Context> &cr) override {
|
||||
if(_has_indicator) {
|
||||
Gtk::CheckMenuItem::draw_indicator_vfunc(cr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Menus
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
static std::string PrepareMenuLabel(std::string label) {
|
||||
std::replace(label.begin(), label.end(), '&', '_');
|
||||
return label;
|
||||
}
|
||||
|
||||
class MenuItemImplGtk : public MenuItem {
|
||||
public:
|
||||
GtkMenuItem gtkMenuItem;
|
||||
|
||||
MenuItemImplGtk() : gtkMenuItem(this) {}
|
||||
|
||||
void SetAccelerator(KeyboardEvent accel) override {
|
||||
guint accelKey;
|
||||
if(accel.key == KeyboardEvent::Key::CHARACTER) {
|
||||
if(accel.chr == '\t') {
|
||||
accelKey = GDK_KEY_Tab;
|
||||
} else if(accel.chr == '\x1b') {
|
||||
accelKey = GDK_KEY_Escape;
|
||||
} else if(accel.chr == '\x7f') {
|
||||
accelKey = GDK_KEY_Delete;
|
||||
} else {
|
||||
accelKey = gdk_unicode_to_keyval(accel.chr);
|
||||
}
|
||||
} else if(accel.key == KeyboardEvent::Key::FUNCTION) {
|
||||
accelKey = GDK_KEY_F1 + accel.num - 1;
|
||||
}
|
||||
|
||||
Gdk::ModifierType accelMods = {};
|
||||
if(accel.shiftDown) {
|
||||
accelMods |= Gdk::SHIFT_MASK;
|
||||
}
|
||||
if(accel.controlDown) {
|
||||
accelMods |= Gdk::CONTROL_MASK;
|
||||
}
|
||||
|
||||
gtkMenuItem.set_accel_key(Gtk::AccelKey(accelKey, accelMods));
|
||||
}
|
||||
|
||||
void SetIndicator(Indicator type) override {
|
||||
switch(type) {
|
||||
case Indicator::NONE:
|
||||
gtkMenuItem.set_has_indicator(false);
|
||||
break;
|
||||
|
||||
case Indicator::CHECK_MARK:
|
||||
gtkMenuItem.set_has_indicator(true);
|
||||
gtkMenuItem.set_draw_as_radio(false);
|
||||
break;
|
||||
|
||||
case Indicator::RADIO_MARK:
|
||||
gtkMenuItem.set_has_indicator(true);
|
||||
gtkMenuItem.set_draw_as_radio(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SetActive(bool active) override {
|
||||
ssassert(gtkMenuItem.has_indicator(),
|
||||
"Cannot change state of a menu item without indicator");
|
||||
gtkMenuItem.set_active(active);
|
||||
}
|
||||
|
||||
void SetEnabled(bool enabled) override {
|
||||
gtkMenuItem.set_sensitive(enabled);
|
||||
}
|
||||
};
|
||||
|
||||
class MenuImplGtk : public Menu {
|
||||
public:
|
||||
Gtk::Menu gtkMenu;
|
||||
std::vector<std::shared_ptr<MenuItemImplGtk>> menuItems;
|
||||
std::vector<std::shared_ptr<MenuImplGtk>> subMenus;
|
||||
|
||||
MenuItemRef AddItem(const std::string &label,
|
||||
std::function<void()> onTrigger = NULL) override {
|
||||
auto menuItem = std::make_shared<MenuItemImplGtk>();
|
||||
menuItems.push_back(menuItem);
|
||||
|
||||
menuItem->gtkMenuItem.set_label(PrepareMenuLabel(label));
|
||||
menuItem->gtkMenuItem.set_use_underline(true);
|
||||
menuItem->gtkMenuItem.show();
|
||||
menuItem->onTrigger = onTrigger;
|
||||
gtkMenu.append(menuItem->gtkMenuItem);
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
auto menuItem = std::make_shared<MenuItemImplGtk>();
|
||||
menuItems.push_back(menuItem);
|
||||
|
||||
auto subMenu = std::make_shared<MenuImplGtk>();
|
||||
subMenus.push_back(subMenu);
|
||||
|
||||
menuItem->gtkMenuItem.set_label(PrepareMenuLabel(label));
|
||||
menuItem->gtkMenuItem.set_use_underline(true);
|
||||
menuItem->gtkMenuItem.set_submenu(subMenu->gtkMenu);
|
||||
menuItem->gtkMenuItem.show_all();
|
||||
gtkMenu.append(menuItem->gtkMenuItem);
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
void AddSeparator() override {
|
||||
Gtk::SeparatorMenuItem *gtkMenuItem = Gtk::manage(new Gtk::SeparatorMenuItem());
|
||||
gtkMenuItem->show();
|
||||
gtkMenu.append(*Gtk::manage(gtkMenuItem));
|
||||
}
|
||||
|
||||
void PopUp() override {
|
||||
Glib::RefPtr<Glib::MainLoop> loop = Glib::MainLoop::create();
|
||||
auto signal = gtkMenu.signal_deactivate().connect([&]() { loop->quit(); });
|
||||
|
||||
gtkMenu.show_all();
|
||||
gtkMenu.popup(0, GDK_CURRENT_TIME);
|
||||
loop->run();
|
||||
signal.disconnect();
|
||||
}
|
||||
|
||||
void Clear() override {
|
||||
gtkMenu.foreach([&](Gtk::Widget &w) { gtkMenu.remove(w); });
|
||||
menuItems.clear();
|
||||
subMenus.clear();
|
||||
}
|
||||
};
|
||||
|
||||
MenuRef CreateMenu() {
|
||||
return std::make_shared<MenuImplGtk>();
|
||||
}
|
||||
|
||||
class MenuBarImplGtk : public MenuBar {
|
||||
public:
|
||||
Gtk::MenuBar gtkMenuBar;
|
||||
std::vector<std::shared_ptr<MenuImplGtk>> subMenus;
|
||||
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
auto subMenu = std::make_shared<MenuImplGtk>();
|
||||
subMenus.push_back(subMenu);
|
||||
|
||||
Gtk::MenuItem *gtkMenuItem = Gtk::manage(new Gtk::MenuItem);
|
||||
gtkMenuItem->set_label(PrepareMenuLabel(label));
|
||||
gtkMenuItem->set_use_underline(true);
|
||||
gtkMenuItem->set_submenu(subMenu->gtkMenu);
|
||||
gtkMenuItem->show_all();
|
||||
gtkMenuBar.append(*gtkMenuItem);
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
void Clear() override {
|
||||
gtkMenuBar.foreach([&](Gtk::Widget &w) { gtkMenuBar.remove(w); });
|
||||
subMenus.clear();
|
||||
}
|
||||
|
||||
void *NativePtr() override {
|
||||
return >kMenuBar;
|
||||
}
|
||||
};
|
||||
|
||||
MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||
*unique = false;
|
||||
return std::make_shared<MenuBarImplGtk>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,10 @@
|
||||
// Objective-C bridging
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
static NSString* Wrap(const std::string &s) {
|
||||
return [NSString stringWithUTF8String:s.c_str()];
|
||||
}
|
||||
|
||||
@interface SSFunction : NSObject
|
||||
- (SSFunction *)initWithFunction:(std::function<void ()> *)aFunc;
|
||||
- (void)run;
|
||||
@ -46,11 +50,11 @@ public:
|
||||
TimerImplCocoa() : timer(NULL) {}
|
||||
|
||||
void WindUp(unsigned milliseconds) override {
|
||||
SSFunction *callback = [[SSFunction alloc] init:&this->onTimeout];
|
||||
SSFunction *callback = [[SSFunction alloc] initWithFunction:&this->onTimeout];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
|
||||
[callback methodSignatureForSelector:@selector(run)]];
|
||||
[invocation setTarget:callback];
|
||||
[invocation setSelector:@selector(run)];
|
||||
invocation.target = callback;
|
||||
invocation.selector = @selector(run);
|
||||
|
||||
if(timer != NULL) {
|
||||
[timer invalidate];
|
||||
@ -70,5 +74,164 @@ TimerRef CreateTimer() {
|
||||
return std::unique_ptr<TimerImplCocoa>(new TimerImplCocoa);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Menus
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
static std::string PrepareMenuLabel(std::string label) {
|
||||
// OS X does not support mnemonics
|
||||
label.erase(std::remove(label.begin(), label.end(), '&'), label.end());
|
||||
return label;
|
||||
}
|
||||
|
||||
class MenuItemImplCocoa : public MenuItem {
|
||||
public:
|
||||
SSFunction *ssFunction;
|
||||
NSMenuItem *nsMenuItem;
|
||||
|
||||
MenuItemImplCocoa() {
|
||||
ssFunction = [[SSFunction alloc] initWithFunction:&onTrigger];
|
||||
nsMenuItem = [[NSMenuItem alloc] initWithTitle:@""
|
||||
action:@selector(run) keyEquivalent:@""];
|
||||
nsMenuItem.target = ssFunction;
|
||||
}
|
||||
|
||||
void SetAccelerator(KeyboardEvent accel) override {
|
||||
unichar accelChar;
|
||||
switch(accel.key) {
|
||||
case KeyboardEvent::Key::CHARACTER:
|
||||
if(accel.chr == NSDeleteCharacter) {
|
||||
accelChar = NSBackspaceCharacter;
|
||||
} else {
|
||||
accelChar = accel.chr;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyboardEvent::Key::FUNCTION:
|
||||
accelChar = NSF1FunctionKey + accel.num - 1;
|
||||
break;
|
||||
}
|
||||
nsMenuItem.keyEquivalent = [[NSString alloc] initWithCharacters:&accelChar length:1];
|
||||
|
||||
NSUInteger modifierMask = 0;
|
||||
if(accel.shiftDown)
|
||||
modifierMask |= NSShiftKeyMask;
|
||||
if(accel.controlDown)
|
||||
modifierMask |= NSCommandKeyMask;
|
||||
nsMenuItem.keyEquivalentModifierMask = modifierMask;
|
||||
}
|
||||
|
||||
void SetIndicator(Indicator state) override {
|
||||
// macOS does not support radio menu items
|
||||
}
|
||||
|
||||
void SetActive(bool active) override {
|
||||
nsMenuItem.state = active ? NSOnState : NSOffState;
|
||||
}
|
||||
|
||||
void SetEnabled(bool enabled) override {
|
||||
nsMenuItem.enabled = enabled;
|
||||
}
|
||||
};
|
||||
|
||||
class MenuImplCocoa : public Menu {
|
||||
public:
|
||||
NSMenu *nsMenu;
|
||||
|
||||
std::vector<std::shared_ptr<MenuItemImplCocoa>> menuItems;
|
||||
std::vector<std::shared_ptr<MenuImplCocoa>> subMenus;
|
||||
|
||||
MenuImplCocoa() {
|
||||
nsMenu = [[NSMenu alloc] initWithTitle:@""];
|
||||
[nsMenu setAutoenablesItems:NO];
|
||||
}
|
||||
|
||||
MenuItemRef AddItem(const std::string &label,
|
||||
std::function<void()> onTrigger = NULL) override {
|
||||
auto menuItem = std::make_shared<MenuItemImplCocoa>();
|
||||
menuItems.push_back(menuItem);
|
||||
|
||||
menuItem->onTrigger = onTrigger;
|
||||
[menuItem->nsMenuItem setTitle:Wrap(PrepareMenuLabel(label))];
|
||||
[nsMenu addItem:menuItem->nsMenuItem];
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
auto subMenu = std::make_shared<MenuImplCocoa>();
|
||||
subMenus.push_back(subMenu);
|
||||
|
||||
NSMenuItem *nsMenuItem =
|
||||
[nsMenu addItemWithTitle:Wrap(PrepareMenuLabel(label)) action:nil keyEquivalent:@""];
|
||||
[nsMenu setSubmenu:subMenu->nsMenu forItem:nsMenuItem];
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
void AddSeparator() override {
|
||||
[nsMenu addItem:[NSMenuItem separatorItem]];
|
||||
}
|
||||
|
||||
void PopUp() override {
|
||||
[NSMenu popUpContextMenu:nsMenu withEvent:[NSApp currentEvent] forView:nil];
|
||||
}
|
||||
|
||||
void Clear() override {
|
||||
[nsMenu removeAllItems];
|
||||
menuItems.clear();
|
||||
subMenus.clear();
|
||||
}
|
||||
};
|
||||
|
||||
MenuRef CreateMenu() {
|
||||
return std::make_shared<MenuImplCocoa>();
|
||||
}
|
||||
|
||||
class MenuBarImplCocoa : public MenuBar {
|
||||
public:
|
||||
NSMenu *nsMenuBar;
|
||||
|
||||
std::vector<std::shared_ptr<MenuImplCocoa>> subMenus;
|
||||
|
||||
MenuBarImplCocoa() {
|
||||
nsMenuBar = [NSApp mainMenu];
|
||||
}
|
||||
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
auto subMenu = std::make_shared<MenuImplCocoa>();
|
||||
subMenus.push_back(subMenu);
|
||||
|
||||
NSMenuItem *nsMenuItem = [nsMenuBar addItemWithTitle:@"" action:nil keyEquivalent:@""];
|
||||
[subMenu->nsMenu setTitle:Wrap(PrepareMenuLabel(label))];
|
||||
[nsMenuBar setSubmenu:subMenu->nsMenu forItem:nsMenuItem];
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
void Clear() override {
|
||||
while([nsMenuBar numberOfItems] != 1) {
|
||||
[nsMenuBar removeItemAtIndex:1];
|
||||
}
|
||||
subMenus.clear();
|
||||
}
|
||||
|
||||
void *NativePtr() override {
|
||||
return NULL;
|
||||
}
|
||||
};
|
||||
|
||||
MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||
static std::shared_ptr<MenuBarImplCocoa> mainMenu;
|
||||
if(!mainMenu) {
|
||||
mainMenu = std::make_shared<MenuBarImplCocoa>();
|
||||
}
|
||||
*unique = true;
|
||||
return mainMenu;
|
||||
}
|
||||
|
||||
void SetMainMenu(MenuBarRef menuBar) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,54 @@ TimerRef CreateTimer() {
|
||||
return std::unique_ptr<Timer>(new TimerImplDummy);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Menus
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
class MenuItemImplDummy : public MenuItem {
|
||||
public:
|
||||
void SetAccelerator(KeyboardEvent accel) override {}
|
||||
void SetIndicator(Indicator type) override {}
|
||||
void SetActive(bool active) override {}
|
||||
void SetEnabled(bool enabled) override {}
|
||||
};
|
||||
|
||||
class MenuImplDummy : public Menu {
|
||||
public:
|
||||
MenuItemRef AddItem(const std::string &label,
|
||||
std::function<void()> onTrigger = NULL) override {
|
||||
return std::make_shared<MenuItemImplDummy>();
|
||||
}
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
return std::make_shared<MenuImplDummy>();
|
||||
}
|
||||
|
||||
void AddSeparator() override {}
|
||||
void PopUp() override {}
|
||||
void Clear() override {}
|
||||
};
|
||||
|
||||
MenuRef CreateMenu() {
|
||||
return std::make_shared<MenuImplDummy>();
|
||||
}
|
||||
|
||||
class MenuBarImplDummy : public MenuBar {
|
||||
public:
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
return std::make_shared<MenuImplDummy>();
|
||||
}
|
||||
void Clear() override {}
|
||||
void *NativePtr() override { return NULL; }
|
||||
};
|
||||
|
||||
MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||
*unique = false;
|
||||
return std::make_shared<MenuBarImplDummy>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SetMainMenu(Platform::MenuBarRef menuBar) {
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -170,23 +218,6 @@ void HideGraphicsEditControl() {
|
||||
bool GraphicsEditControlIsVisible() {
|
||||
return false;
|
||||
}
|
||||
void AddContextMenuItem(const char *label, ContextCommand cmd) {
|
||||
ssassert(false, "Not implemented");
|
||||
}
|
||||
void CreateContextSubmenu() {
|
||||
ssassert(false, "Not implemented");
|
||||
}
|
||||
ContextCommand ShowContextMenu() {
|
||||
ssassert(false, "Not implemented");
|
||||
}
|
||||
void EnableMenuByCmd(Command cmd, bool enabled) {
|
||||
}
|
||||
void CheckMenuByCmd(Command cmd, bool checked) {
|
||||
}
|
||||
void RadioMenuByCmd(Command cmd, bool selected) {
|
||||
}
|
||||
void RefreshRecentMenus() {
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Text window
|
||||
|
@ -76,5 +76,216 @@ TimerRef CreateTimer() {
|
||||
return std::unique_ptr<TimerImplWin32>(new TimerImplWin32);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Menus
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
class MenuImplWin32;
|
||||
|
||||
class MenuItemImplWin32 : public MenuItem {
|
||||
public:
|
||||
std::shared_ptr<MenuImplWin32> menu;
|
||||
|
||||
HMENU Handle();
|
||||
|
||||
MENUITEMINFOW GetInfo(UINT mask) {
|
||||
MENUITEMINFOW mii = {};
|
||||
mii.cbSize = sizeof(mii);
|
||||
mii.fMask = mask;
|
||||
sscheck(GetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
||||
return mii;
|
||||
}
|
||||
|
||||
void SetAccelerator(KeyboardEvent accel) override {
|
||||
MENUITEMINFOW mii = GetInfo(MIIM_TYPE);
|
||||
|
||||
std::wstring nameW(mii.cch, L'\0');
|
||||
mii.dwTypeData = &nameW[0];
|
||||
mii.cch++;
|
||||
sscheck(GetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
||||
|
||||
std::string name = Narrow(nameW);
|
||||
if(name.find('\t') != std::string::npos) {
|
||||
name = name.substr(0, name.find('\t'));
|
||||
}
|
||||
name += '\t';
|
||||
name += AcceleratorDescription(accel);
|
||||
|
||||
nameW = Widen(name);
|
||||
mii.fMask = MIIM_STRING;
|
||||
mii.dwTypeData = &nameW[0];
|
||||
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
||||
}
|
||||
|
||||
void SetIndicator(Indicator type) override {
|
||||
MENUITEMINFOW mii = GetInfo(MIIM_FTYPE);
|
||||
switch(type) {
|
||||
case Indicator::NONE:
|
||||
case Indicator::CHECK_MARK:
|
||||
mii.fType &= ~MFT_RADIOCHECK;
|
||||
break;
|
||||
|
||||
case Indicator::RADIO_MARK:
|
||||
mii.fType |= MFT_RADIOCHECK;
|
||||
break;
|
||||
}
|
||||
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
||||
}
|
||||
|
||||
void SetActive(bool active) override {
|
||||
MENUITEMINFOW mii = GetInfo(MIIM_STATE);
|
||||
if(active) {
|
||||
mii.fState |= MFS_CHECKED;
|
||||
} else {
|
||||
mii.fState &= ~MFS_CHECKED;
|
||||
}
|
||||
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
||||
}
|
||||
|
||||
void SetEnabled(bool enabled) override {
|
||||
MENUITEMINFOW mii = GetInfo(MIIM_STATE);
|
||||
if(enabled) {
|
||||
mii.fState &= ~(MFS_DISABLED|MFS_GRAYED);
|
||||
} else {
|
||||
mii.fState |= MFS_DISABLED|MFS_GRAYED;
|
||||
}
|
||||
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
||||
}
|
||||
};
|
||||
|
||||
void TriggerMenu(int id) {
|
||||
MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)id;
|
||||
if(menuItem->onTrigger) {
|
||||
menuItem->onTrigger();
|
||||
}
|
||||
}
|
||||
|
||||
int64_t contextMenuCancelTime = 0;
|
||||
|
||||
class MenuImplWin32 : public Menu {
|
||||
public:
|
||||
HMENU hMenu;
|
||||
|
||||
std::weak_ptr<MenuImplWin32> weakThis;
|
||||
std::vector<std::shared_ptr<MenuItemImplWin32>> menuItems;
|
||||
std::vector<std::shared_ptr<MenuImplWin32>> subMenus;
|
||||
|
||||
MenuImplWin32() {
|
||||
sscheck(hMenu = CreatePopupMenu());
|
||||
|
||||
MENUINFO mi = {};
|
||||
mi.cbSize = sizeof(mi);
|
||||
mi.fMask = MIM_STYLE;
|
||||
mi.dwStyle = MNS_NOTIFYBYPOS;
|
||||
sscheck(SetMenuInfo(hMenu, &mi));
|
||||
}
|
||||
|
||||
MenuItemRef AddItem(const std::string &label,
|
||||
std::function<void()> onTrigger = NULL) override {
|
||||
auto menuItem = std::make_shared<MenuItemImplWin32>();
|
||||
menuItem->menu = weakThis.lock();
|
||||
menuItem->onTrigger = onTrigger;
|
||||
menuItems.push_back(menuItem);
|
||||
|
||||
sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)&*menuItem, Widen(label).c_str()));
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
auto subMenu = std::make_shared<MenuImplWin32>();
|
||||
subMenu->weakThis = subMenu;
|
||||
subMenus.push_back(subMenu);
|
||||
|
||||
sscheck(AppendMenuW(hMenu, MF_STRING|MF_POPUP,
|
||||
(UINT_PTR)subMenu->hMenu, Widen(label).c_str()));
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
void AddSeparator() override {
|
||||
sscheck(AppendMenuW(hMenu, MF_SEPARATOR, 0, L""));
|
||||
}
|
||||
|
||||
void PopUp() override {
|
||||
POINT pt;
|
||||
sscheck(GetCursorPos(&pt));
|
||||
int id = TrackPopupMenu(hMenu, TPM_TOPALIGN|TPM_RIGHTBUTTON|TPM_RETURNCMD,
|
||||
pt.x, pt.y, 0, GetActiveWindow(), NULL);
|
||||
if(id == 0) {
|
||||
contextMenuCancelTime = GetMilliseconds();
|
||||
} else {
|
||||
TriggerMenu(id);
|
||||
}
|
||||
}
|
||||
|
||||
void Clear() override {
|
||||
for(int n = GetMenuItemCount(hMenu) - 1; n >= 0; n--) {
|
||||
sscheck(RemoveMenu(hMenu, n, MF_BYPOSITION));
|
||||
}
|
||||
menuItems.clear();
|
||||
subMenus.clear();
|
||||
}
|
||||
|
||||
~MenuImplWin32() {
|
||||
Clear();
|
||||
sscheck(DestroyMenu(hMenu));
|
||||
}
|
||||
};
|
||||
|
||||
HMENU MenuItemImplWin32::Handle() {
|
||||
return menu->hMenu;
|
||||
}
|
||||
|
||||
MenuRef CreateMenu() {
|
||||
auto menu = std::make_shared<MenuImplWin32>();
|
||||
// std::enable_shared_from_this fails for some reason, not sure why
|
||||
menu->weakThis = menu;
|
||||
return menu;
|
||||
}
|
||||
|
||||
class MenuBarImplWin32 : public MenuBar {
|
||||
public:
|
||||
HMENU hMenuBar;
|
||||
|
||||
std::vector<std::shared_ptr<MenuImplWin32>> subMenus;
|
||||
|
||||
MenuBarImplWin32() {
|
||||
sscheck(hMenuBar = ::CreateMenu());
|
||||
}
|
||||
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
auto subMenu = std::make_shared<MenuImplWin32>();
|
||||
subMenu->weakThis = subMenu;
|
||||
subMenus.push_back(subMenu);
|
||||
|
||||
sscheck(AppendMenuW(hMenuBar, MF_STRING|MF_POPUP,
|
||||
(UINT_PTR)subMenu->hMenu, Widen(label).c_str()));
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
void Clear() override {
|
||||
for(int n = GetMenuItemCount(hMenuBar) - 1; n >= 0; n--) {
|
||||
sscheck(RemoveMenu(hMenuBar, n, MF_BYPOSITION));
|
||||
}
|
||||
subMenus.clear();
|
||||
}
|
||||
|
||||
~MenuBarImplWin32() {
|
||||
Clear();
|
||||
sscheck(DestroyMenu(hMenuBar));
|
||||
}
|
||||
|
||||
void *NativePtr() override {
|
||||
return hMenuBar;
|
||||
}
|
||||
};
|
||||
|
||||
MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||
*unique = false;
|
||||
return std::make_shared<MenuBarImplWin32>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -61,11 +61,6 @@ static struct {
|
||||
int x, y;
|
||||
} LastMousePos;
|
||||
|
||||
HMENU SubMenus[100];
|
||||
HMENU RecentOpenMenu, RecentImportMenu;
|
||||
|
||||
HMENU ContextMenu, ContextSubmenu;
|
||||
|
||||
int ClientIsSmallerBy;
|
||||
|
||||
HFONT FixedFont;
|
||||
@ -214,42 +209,6 @@ void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error)
|
||||
DestroyWindow(MessageWnd);
|
||||
}
|
||||
|
||||
void SolveSpace::AddContextMenuItem(const char *label, ContextCommand cmd)
|
||||
{
|
||||
if(!ContextMenu) ContextMenu = CreatePopupMenu();
|
||||
|
||||
if(cmd == ContextCommand::SUBMENU) {
|
||||
AppendMenuW(ContextMenu, MF_STRING | MF_POPUP,
|
||||
(UINT_PTR)ContextSubmenu, Widen(label).c_str());
|
||||
ContextSubmenu = NULL;
|
||||
} else {
|
||||
HMENU m = ContextSubmenu ? ContextSubmenu : ContextMenu;
|
||||
if(cmd == ContextCommand::SEPARATOR) {
|
||||
AppendMenuW(m, MF_SEPARATOR, 0, L"");
|
||||
} else {
|
||||
AppendMenuW(m, MF_STRING, (uint32_t)cmd, Widen(label).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SolveSpace::CreateContextSubmenu()
|
||||
{
|
||||
ContextSubmenu = CreatePopupMenu();
|
||||
}
|
||||
|
||||
ContextCommand SolveSpace::ShowContextMenu()
|
||||
{
|
||||
POINT p;
|
||||
GetCursorPos(&p);
|
||||
int r = TrackPopupMenu(ContextMenu,
|
||||
TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_TOPALIGN,
|
||||
p.x, p.y, 0, GraphicsWnd, NULL);
|
||||
|
||||
DestroyMenu(ContextMenu);
|
||||
ContextMenu = NULL;
|
||||
return (ContextCommand)r;
|
||||
}
|
||||
|
||||
static void GetWindowSize(HWND hwnd, int *w, int *h)
|
||||
{
|
||||
RECT r;
|
||||
@ -665,68 +624,35 @@ static bool ProcessKeyDown(WPARAM wParam)
|
||||
}
|
||||
}
|
||||
|
||||
int c;
|
||||
switch(wParam) {
|
||||
case VK_OEM_PLUS: c = '+'; break;
|
||||
case VK_OEM_MINUS: c = '-'; break;
|
||||
case VK_ESCAPE: c = 27; break;
|
||||
case VK_OEM_1: c = ';'; break;
|
||||
case VK_OEM_3: c = '`'; break;
|
||||
case VK_OEM_4: c = '['; break;
|
||||
case VK_OEM_6: c = ']'; break;
|
||||
case VK_OEM_5: c = '\\'; break;
|
||||
case VK_OEM_PERIOD: c = '.'; break;
|
||||
case VK_DECIMAL: c = '.'; break;
|
||||
case VK_SPACE: c = ' '; break;
|
||||
case VK_DELETE: c = 127; break;
|
||||
case VK_TAB: c = '\t'; break;
|
||||
Platform::KeyboardEvent event = {};
|
||||
event.type = Platform::KeyboardEvent::Type::PRESS;
|
||||
|
||||
case VK_BROWSER_BACK:
|
||||
case VK_BACK: c = '\b'; break;
|
||||
if(GetAsyncKeyState(VK_SHIFT) & 0x8000)
|
||||
event.shiftDown = true;
|
||||
if(GetAsyncKeyState(VK_CONTROL) & 0x8000)
|
||||
event.controlDown = true;
|
||||
|
||||
case VK_F1:
|
||||
case VK_F2:
|
||||
case VK_F3:
|
||||
case VK_F4:
|
||||
case VK_F5:
|
||||
case VK_F6:
|
||||
case VK_F7:
|
||||
case VK_F8:
|
||||
case VK_F9:
|
||||
case VK_F10:
|
||||
case VK_F11:
|
||||
case VK_F12: c = ((int)wParam - VK_F1) + 0xf1; break;
|
||||
|
||||
// These overlap with some character codes that I'm using, so
|
||||
// don't let them trigger by accident.
|
||||
case VK_F16:
|
||||
case VK_INSERT:
|
||||
case VK_EXECUTE:
|
||||
case VK_APPS:
|
||||
case VK_LWIN:
|
||||
case VK_RWIN: return false;
|
||||
|
||||
default:
|
||||
c = (int)wParam;
|
||||
break;
|
||||
}
|
||||
if(GetAsyncKeyState(VK_SHIFT) & 0x8000) c |= GraphicsWindow::SHIFT_MASK;
|
||||
if(GetAsyncKeyState(VK_CONTROL) & 0x8000) c |= GraphicsWindow::CTRL_MASK;
|
||||
|
||||
switch(c) {
|
||||
case GraphicsWindow::SHIFT_MASK | '.': c = '>'; break;
|
||||
}
|
||||
|
||||
for(int i = 0; SS.GW.menu[i].level >= 0; i++) {
|
||||
if(c == SS.GW.menu[i].accel) {
|
||||
(SS.GW.menu[i].fn)((Command)SS.GW.menu[i].id);
|
||||
break;
|
||||
if(wParam >= VK_F1 && wParam <= VK_F12) {
|
||||
event.key = Platform::KeyboardEvent::Key::FUNCTION;
|
||||
event.num = wParam - VK_F1 + 1;
|
||||
} else {
|
||||
event.key = Platform::KeyboardEvent::Key::CHARACTER;
|
||||
event.chr = tolower(MapVirtualKeyW(wParam, MAPVK_VK_TO_CHAR));
|
||||
if(event.chr == 0) {
|
||||
if(wParam == VK_DELETE) {
|
||||
event.chr = '\x7f';
|
||||
} else {
|
||||
// Non-mappable key.
|
||||
return false;
|
||||
}
|
||||
} else if(event.chr == '.' && event.shiftDown) {
|
||||
event.chr = '>';
|
||||
event.shiftDown = false;;
|
||||
}
|
||||
}
|
||||
|
||||
if(SS.GW.KeyDown(c)) return true;
|
||||
if(SS.GW.KeyboardEvent(event)) return true;
|
||||
|
||||
// No accelerator; process the key as normal.
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -927,6 +853,13 @@ bool SolveSpace::GraphicsEditControlIsVisible()
|
||||
return IsWindowVisible(GraphicsEditControl) ? true : false;
|
||||
}
|
||||
|
||||
namespace SolveSpace {
|
||||
namespace Platform {
|
||||
void TriggerMenu(int id);
|
||||
extern int64_t contextMenuCancelTime;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK GraphicsWndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
LPARAM lParam)
|
||||
{
|
||||
@ -961,6 +894,12 @@ LRESULT CALLBACK GraphicsWndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_MBUTTONDOWN: {
|
||||
if(GetMilliseconds() - Platform::contextMenuCancelTime < 100) {
|
||||
// Ignore the mouse click that dismisses a context menu, to avoid
|
||||
// (e.g.) clearing a selection.
|
||||
return 1;
|
||||
}
|
||||
|
||||
int x = LOWORD(lParam);
|
||||
int y = HIWORD(lParam);
|
||||
|
||||
@ -1004,33 +943,8 @@ LRESULT CALLBACK GraphicsWndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
MouseWheel(GET_WHEEL_DELTA_WPARAM(wParam));
|
||||
break;
|
||||
|
||||
case WM_COMMAND: {
|
||||
if(HIWORD(wParam) == 0) {
|
||||
Command id = (Command)LOWORD(wParam);
|
||||
if(((uint32_t)id >= (uint32_t)Command::RECENT_OPEN &&
|
||||
(uint32_t)id < ((uint32_t)Command::RECENT_OPEN + MAX_RECENT))) {
|
||||
SolveSpaceUI::MenuFile(id);
|
||||
break;
|
||||
}
|
||||
if(((uint32_t)id >= (uint32_t)Command::RECENT_LINK &&
|
||||
(uint32_t)id < ((uint32_t)Command::RECENT_LINK + MAX_RECENT))) {
|
||||
Group::MenuGroup(id);
|
||||
break;
|
||||
}
|
||||
if((uint32_t)id >= (uint32_t)Command::LOCALE &&
|
||||
(uint32_t)id < ((uint32_t)Command::LOCALE + Locales().size())) {
|
||||
SolveSpaceUI::MenuHelp(id);
|
||||
break;
|
||||
}
|
||||
int i;
|
||||
for(i = 0; SS.GW.menu[i].level >= 0; i++) {
|
||||
if(id == SS.GW.menu[i].id) {
|
||||
(SS.GW.menu[i].fn)((Command)id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ssassert(SS.GW.menu[i].level >= 0, "Cannot find command in the menu");
|
||||
}
|
||||
case WM_MENUCOMMAND: {
|
||||
SolveSpace::Platform::TriggerMenu(GetMenuItemID((HMENU)lParam, wParam));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1233,114 +1147,6 @@ std::vector<Platform::Path> SolveSpace::GetFontFiles() {
|
||||
return fonts;
|
||||
}
|
||||
|
||||
static void MenuByCmd(Command id, bool yes, bool check)
|
||||
{
|
||||
int i;
|
||||
int subMenu = -1;
|
||||
|
||||
for(i = 0; SS.GW.menu[i].level >= 0; i++) {
|
||||
if(SS.GW.menu[i].level == 0) subMenu++;
|
||||
|
||||
if(SS.GW.menu[i].id == id) {
|
||||
ssassert(subMenu >= 0 && subMenu < (int)arraylen(SubMenus),
|
||||
"Submenu out of range");
|
||||
|
||||
if(check) {
|
||||
CheckMenuItem(SubMenus[subMenu], (uint32_t)id,
|
||||
yes ? MF_CHECKED : MF_UNCHECKED);
|
||||
} else {
|
||||
EnableMenuItem(SubMenus[subMenu], (uint32_t)id,
|
||||
yes ? MF_ENABLED : MF_GRAYED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
ssassert(false, "Cannot find submenu");
|
||||
}
|
||||
void SolveSpace::CheckMenuByCmd(Command cmd, bool checked)
|
||||
{
|
||||
MenuByCmd(cmd, checked, true);
|
||||
}
|
||||
void SolveSpace::RadioMenuByCmd(Command cmd, bool selected)
|
||||
{
|
||||
// Windows does not natively support radio-button menu items
|
||||
MenuByCmd(cmd, selected, true);
|
||||
}
|
||||
void SolveSpace::EnableMenuByCmd(Command cmd, bool enabled)
|
||||
{
|
||||
MenuByCmd(cmd, enabled, false);
|
||||
}
|
||||
static void DoRecent(HMENU m, Command base)
|
||||
{
|
||||
while(DeleteMenu(m, 0, MF_BYPOSITION))
|
||||
;
|
||||
int c = 0;
|
||||
for(size_t i = 0; i < MAX_RECENT; i++) {
|
||||
if(!RecentFile[i].IsEmpty()) {
|
||||
AppendMenuW(m, MF_STRING, (uint32_t)base + i, Widen(RecentFile[i].raw).c_str());
|
||||
c++;
|
||||
}
|
||||
}
|
||||
if(c == 0) AppendMenuW(m, MF_STRING | MF_GRAYED, 0, Widen(_("(no recent files)")).c_str());
|
||||
}
|
||||
void SolveSpace::RefreshRecentMenus()
|
||||
{
|
||||
DoRecent(RecentOpenMenu, Command::RECENT_OPEN);
|
||||
DoRecent(RecentImportMenu, Command::RECENT_LINK);
|
||||
}
|
||||
|
||||
HMENU CreateGraphicsWindowMenus()
|
||||
{
|
||||
HMENU top = CreateMenu();
|
||||
HMENU m = 0;
|
||||
|
||||
int i;
|
||||
int subMenu = 0;
|
||||
|
||||
for(i = 0; SS.GW.menu[i].level >= 0; i++) {
|
||||
std::string label;
|
||||
if(SS.GW.menu[i].label) {
|
||||
std::string accel = MakeAcceleratorLabel(SS.GW.menu[i].accel);
|
||||
const char *sep = accel.empty() ? "" : "\t";
|
||||
label = ssprintf("%s%s%s", Translate(SS.GW.menu[i].label).c_str(), sep, accel.c_str());
|
||||
}
|
||||
|
||||
if(SS.GW.menu[i].level == 0) {
|
||||
m = CreateMenu();
|
||||
AppendMenuW(top, MF_STRING | MF_POPUP, (UINT_PTR)m, Widen(label).c_str());
|
||||
ssassert(subMenu < (int)arraylen(SubMenus), "Too many submenus");
|
||||
SubMenus[subMenu] = m;
|
||||
subMenu++;
|
||||
} else if(SS.GW.menu[i].level == 1) {
|
||||
if(SS.GW.menu[i].id == Command::OPEN_RECENT) {
|
||||
RecentOpenMenu = CreateMenu();
|
||||
AppendMenuW(m, MF_STRING | MF_POPUP,
|
||||
(UINT_PTR)RecentOpenMenu, Widen(label).c_str());
|
||||
} else if(SS.GW.menu[i].id == Command::GROUP_RECENT) {
|
||||
RecentImportMenu = CreateMenu();
|
||||
AppendMenuW(m, MF_STRING | MF_POPUP,
|
||||
(UINT_PTR)RecentImportMenu, Widen(label).c_str());
|
||||
} else if(SS.GW.menu[i].id == Command::LOCALE) {
|
||||
HMENU LocaleMenu = CreateMenu();
|
||||
size_t i = 0;
|
||||
for(auto locale : Locales()) {
|
||||
AppendMenuW(LocaleMenu, MF_STRING,
|
||||
(uint32_t)Command::LOCALE + i++, Widen(locale.displayName).c_str());
|
||||
}
|
||||
AppendMenuW(m, MF_STRING | MF_POPUP,
|
||||
(UINT_PTR)LocaleMenu, Widen(label).c_str());
|
||||
} else if(SS.GW.menu[i].label) {
|
||||
AppendMenuW(m, MF_STRING, (uint32_t)SS.GW.menu[i].id, Widen(label).c_str());
|
||||
} else {
|
||||
AppendMenuW(m, MF_SEPARATOR, (uint32_t)SS.GW.menu[i].id, L"");
|
||||
}
|
||||
} else ssassert(false, "Submenus nested too deeply");
|
||||
}
|
||||
RefreshRecentMenus();
|
||||
|
||||
return top;
|
||||
}
|
||||
|
||||
static void CreateMainWindows()
|
||||
{
|
||||
WNDCLASSEX wc = {};
|
||||
@ -1414,16 +1220,12 @@ static void CreateMainWindows()
|
||||
ClientIsSmallerBy = (r.bottom - r.top) - (rc.bottom - rc.top);
|
||||
}
|
||||
|
||||
void SolveSpace::RefreshLocale() {
|
||||
void SolveSpace::SetMainMenu(Platform::MenuBarRef menuBar) {
|
||||
static Platform::MenuBarRef _menuBar;
|
||||
SetMenu(GraphicsWnd, (HMENU)menuBar->NativePtr());
|
||||
_menuBar = menuBar;
|
||||
|
||||
SS.UpdateWindowTitle();
|
||||
|
||||
HMENU oldMenu = GetMenu(GraphicsWnd);
|
||||
SetMenu(GraphicsWnd, CreateGraphicsWindowMenus());
|
||||
if(oldMenu != NULL) {
|
||||
DestroyMenu(oldMenu);
|
||||
}
|
||||
RefreshRecentMenus();
|
||||
|
||||
SetWindowTextW(TextWnd, Title(C_("title", "Property Browser")).c_str());
|
||||
}
|
||||
|
||||
|
@ -1514,7 +1514,6 @@ bool SetLocale(Predicate pred) {
|
||||
std::string filename = "locales/" + it->language + "_" + it->region + ".po";
|
||||
translations[*it] = Translation::From(LoadString(filename));
|
||||
currentTranslation = &translations[*it];
|
||||
RefreshLocale();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -292,6 +292,7 @@ public:
|
||||
SPolygon GetPolygon();
|
||||
|
||||
static void MenuGroup(Command id);
|
||||
static void MenuGroup(Command id, Platform::Path linkFile);
|
||||
};
|
||||
|
||||
// A user request for some primitive or derived operation; for example a
|
||||
|
@ -10,8 +10,6 @@
|
||||
SolveSpaceUI SolveSpace::SS = {};
|
||||
Sketch SolveSpace::SK = {};
|
||||
|
||||
Platform::Path SolveSpace::RecentFile[MAX_RECENT] = {};
|
||||
|
||||
void SolveSpaceUI::Init() {
|
||||
#if !defined(HEADLESS)
|
||||
// Check that the resource system works.
|
||||
@ -99,9 +97,10 @@ void SolveSpaceUI::Init() {
|
||||
showToolbar = CnfThawBool(true, "ShowToolbar");
|
||||
// Recent files menus
|
||||
for(size_t i = 0; i < MAX_RECENT; i++) {
|
||||
RecentFile[i] = Platform::Path::From(CnfThawString("", "RecentFile_" + std::to_string(i)));
|
||||
std::string rawPath = CnfThawString("", "RecentFile_" + std::to_string(i));
|
||||
if(rawPath.empty()) continue;
|
||||
recentFiles.push_back(Platform::Path::From(rawPath));
|
||||
}
|
||||
RefreshRecentMenus();
|
||||
// Autosave timer
|
||||
autosaveInterval = CnfThawInt(5, "AutosaveInterval");
|
||||
// Locale
|
||||
@ -163,8 +162,13 @@ bool SolveSpaceUI::Load(const Platform::Path &filename) {
|
||||
|
||||
void SolveSpaceUI::Exit() {
|
||||
// Recent files
|
||||
for(size_t i = 0; i < MAX_RECENT; i++)
|
||||
CnfFreezeString(RecentFile[i].raw, "RecentFile_" + std::to_string(i));
|
||||
for(size_t i = 0; i < MAX_RECENT; i++) {
|
||||
std::string rawPath;
|
||||
if(recentFiles.size() > i) {
|
||||
rawPath = recentFiles[i].raw;
|
||||
}
|
||||
CnfFreezeString(rawPath, "RecentFile_" + std::to_string(i));
|
||||
}
|
||||
// Model colors
|
||||
for(size_t i = 0; i < MODEL_COLORS; i++)
|
||||
CnfFreezeColor(modelColor[i], "ModelColor_" + std::to_string(i));
|
||||
@ -354,25 +358,19 @@ void SolveSpaceUI::AfterNewFile() {
|
||||
UpdateWindowTitle();
|
||||
}
|
||||
|
||||
void SolveSpaceUI::RemoveFromRecentList(const Platform::Path &filename) {
|
||||
int dest = 0;
|
||||
for(int src = 0; src < (int)MAX_RECENT; src++) {
|
||||
if(!filename.Equals(RecentFile[src])) {
|
||||
if(src != dest) RecentFile[dest] = RecentFile[src];
|
||||
dest++;
|
||||
}
|
||||
}
|
||||
while(dest < (int)MAX_RECENT) RecentFile[dest++].Clear();
|
||||
RefreshRecentMenus();
|
||||
}
|
||||
void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) {
|
||||
RemoveFromRecentList(filename);
|
||||
|
||||
for(int src = MAX_RECENT - 2; src >= 0; src--) {
|
||||
RecentFile[src+1] = RecentFile[src];
|
||||
auto it = std::find_if(recentFiles.begin(), recentFiles.end(),
|
||||
[&](const Platform::Path &p) { return p.Equals(filename); });
|
||||
if(it != recentFiles.end()) {
|
||||
recentFiles.erase(it);
|
||||
}
|
||||
RecentFile[0] = filename;
|
||||
RefreshRecentMenus();
|
||||
|
||||
if(recentFiles.size() > MAX_RECENT) {
|
||||
recentFiles.erase(recentFiles.begin() + MAX_RECENT);
|
||||
}
|
||||
|
||||
recentFiles.insert(recentFiles.begin(), filename);
|
||||
GW.PopulateRecentFiles();
|
||||
}
|
||||
|
||||
bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
|
||||
@ -430,15 +428,6 @@ void SolveSpaceUI::UpdateWindowTitle() {
|
||||
}
|
||||
|
||||
void SolveSpaceUI::MenuFile(Command id) {
|
||||
if((uint32_t)id >= (uint32_t)Command::RECENT_OPEN &&
|
||||
(uint32_t)id < ((uint32_t)Command::RECENT_OPEN+MAX_RECENT)) {
|
||||
if(!SS.OkayToStartNewFile()) return;
|
||||
|
||||
Platform::Path newFile = RecentFile[(uint32_t)id - (uint32_t)Command::RECENT_OPEN];
|
||||
SS.Load(newFile);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(id) {
|
||||
case Command::NEW:
|
||||
if(!SS.OkayToStartNewFile()) break;
|
||||
@ -831,20 +820,6 @@ void SolveSpaceUI::ShowNakedEdges(bool reportOnlyWhenNotOkay) {
|
||||
}
|
||||
|
||||
void SolveSpaceUI::MenuHelp(Command id) {
|
||||
if((uint32_t)id >= (uint32_t)Command::LOCALE &&
|
||||
(uint32_t)id < ((uint32_t)Command::LOCALE + Locales().size())) {
|
||||
size_t offset = (uint32_t)id - (uint32_t)Command::LOCALE;
|
||||
size_t i = 0;
|
||||
for(auto locale : Locales()) {
|
||||
if(i++ == offset) {
|
||||
CnfFreezeString(locale.Culture(), "Locale");
|
||||
SetLocale(locale.Culture());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch(id) {
|
||||
case Command::WEBSITE:
|
||||
OpenWebsite("http://solvespace.com/helpmenu");
|
||||
|
@ -129,7 +129,6 @@ class ExprVector;
|
||||
class ExprQuaternion;
|
||||
class RgbaColor;
|
||||
enum class Command : uint32_t;
|
||||
enum class ContextCommand : uint32_t;
|
||||
|
||||
//================
|
||||
// From the platform-specific code.
|
||||
@ -165,11 +164,7 @@ std::vector<Platform::Path> GetFontFiles();
|
||||
|
||||
void OpenWebsite(const char *url);
|
||||
|
||||
void RefreshLocale();
|
||||
|
||||
void CheckMenuByCmd(Command id, bool checked);
|
||||
void RadioMenuByCmd(Command id, bool selected);
|
||||
void EnableMenuByCmd(Command id, bool enabled);
|
||||
void SetMainMenu(Platform::MenuBarRef menuBar);
|
||||
|
||||
void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars,
|
||||
const std::string &str);
|
||||
@ -180,10 +175,6 @@ void HideTextEditControl();
|
||||
bool TextEditControlIsVisible();
|
||||
void MoveTextScrollbarTo(int pos, int maxPos, int page);
|
||||
|
||||
void AddContextMenuItem(const char *legend, ContextCommand id);
|
||||
void CreateContextSubmenu();
|
||||
ContextCommand ShowContextMenu();
|
||||
|
||||
void ShowTextWindow(bool visible);
|
||||
void InvalidateText();
|
||||
void InvalidateGraphics();
|
||||
@ -292,7 +283,6 @@ void MakeMatrix(double *mat, double a11, double a12, double a13, double a14,
|
||||
double a41, double a42, double a43, double a44);
|
||||
void MultMatrix(double *mata, double *matb, double *matr);
|
||||
|
||||
std::string MakeAcceleratorLabel(int accel);
|
||||
void Message(const char *str, ...);
|
||||
void Error(const char *str, ...);
|
||||
void CnfFreezeBool(bool v, const std::string &name);
|
||||
@ -704,15 +694,13 @@ public:
|
||||
|
||||
// The platform-dependent code calls this before entering the msg loop
|
||||
void Init();
|
||||
bool Load(const Platform::Path &filename);
|
||||
void Exit();
|
||||
|
||||
// File load/save routines, including the additional files that get
|
||||
// loaded when we have link groups.
|
||||
FILE *fh;
|
||||
void AfterNewFile();
|
||||
static void RemoveFromRecentList(const Platform::Path &filename);
|
||||
static void AddToRecentList(const Platform::Path &filename);
|
||||
void AddToRecentList(const Platform::Path &filename);
|
||||
Platform::Path saveFile;
|
||||
bool fileLoadError;
|
||||
bool unsaved;
|
||||
@ -736,6 +724,9 @@ public:
|
||||
static void MenuFile(Command id);
|
||||
void Autosave();
|
||||
void RemoveAutosave();
|
||||
static const size_t MAX_RECENT = 8;
|
||||
std::vector<Platform::Path> recentFiles;
|
||||
bool Load(const Platform::Path &filename);
|
||||
bool GetFilenameAndSave(bool saveAs);
|
||||
bool OkayToStartNewFile();
|
||||
hGroup CreateDefaultDrawingGroup();
|
||||
|
@ -110,17 +110,9 @@ bool GraphicsWindow::ToolbarMouseDown(int x, int y) {
|
||||
x += ((int)width/2);
|
||||
y += ((int)height/2);
|
||||
|
||||
Command nh = Command::NONE;
|
||||
bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &nh);
|
||||
// They might have clicked within the toolbar, but not on a button.
|
||||
if(withinToolbar && nh != Command::NONE) {
|
||||
for(int i = 0; SS.GW.menu[i].level >= 0; i++) {
|
||||
if(nh == SS.GW.menu[i].id) {
|
||||
(SS.GW.menu[i].fn)((Command)SS.GW.menu[i].id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Command hit;
|
||||
bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hit);
|
||||
SS.GW.ActivateCommand(hit);
|
||||
return withinToolbar;
|
||||
}
|
||||
|
||||
@ -205,5 +197,9 @@ bool GraphicsWindow::ToolbarDrawOrHitTest(int mx, int my,
|
||||
}
|
||||
}
|
||||
|
||||
if(!withinToolbar) {
|
||||
if(menuHit) *menuHit = Command::NONE;
|
||||
}
|
||||
|
||||
return withinToolbar;
|
||||
}
|
||||
|
89
src/ui.h
89
src/ui.h
@ -219,41 +219,9 @@ enum class Command : uint32_t {
|
||||
STOP_TRACING,
|
||||
STEP_DIM,
|
||||
// Help
|
||||
LOCALE,
|
||||
WEBSITE,
|
||||
ABOUT,
|
||||
// Recent
|
||||
RECENT_OPEN = 0xf000,
|
||||
RECENT_LINK = 0xf100,
|
||||
// Locale
|
||||
LOCALE = 0xf200,
|
||||
};
|
||||
|
||||
enum class ContextCommand : uint32_t {
|
||||
CANCELLED,
|
||||
SUBMENU,
|
||||
SEPARATOR,
|
||||
UNSELECT_ALL,
|
||||
UNSELECT_HOVERED,
|
||||
CUT_SEL,
|
||||
COPY_SEL,
|
||||
PASTE,
|
||||
PASTE_XFRM,
|
||||
DELETE_SEL,
|
||||
SELECT_CHAIN,
|
||||
NEW_CUSTOM_STYLE,
|
||||
NO_STYLE,
|
||||
GROUP_INFO,
|
||||
STYLE_INFO,
|
||||
REFERENCE_DIM,
|
||||
OTHER_ANGLE,
|
||||
DEL_COINCIDENT,
|
||||
SNAP_TO_GRID,
|
||||
REMOVE_SPLINE_PT,
|
||||
ADD_SPLINE_PT,
|
||||
CONSTRUCTION,
|
||||
ZOOM_TO_FIT,
|
||||
SELECT_ALL,
|
||||
FIRST_STYLE = 0x40000000
|
||||
};
|
||||
|
||||
class Button;
|
||||
@ -575,30 +543,13 @@ class GraphicsWindow {
|
||||
public:
|
||||
void Init();
|
||||
|
||||
typedef void MenuHandler(Command id);
|
||||
enum {
|
||||
ESCAPE_KEY = 27,
|
||||
DELETE_KEY = 127,
|
||||
FUNCTION_KEY_BASE = 0xf0
|
||||
};
|
||||
enum {
|
||||
SHIFT_MASK = 0x100,
|
||||
CTRL_MASK = 0x200
|
||||
};
|
||||
enum class MenuKind : uint32_t {
|
||||
NORMAL = 0,
|
||||
CHECK,
|
||||
RADIO
|
||||
};
|
||||
typedef struct {
|
||||
int level; // 0 == on menu bar, 1 == one level down
|
||||
const char *label; // or NULL for a separator
|
||||
Command id; // unique ID
|
||||
int accel; // keyboard accelerator
|
||||
MenuKind kind;
|
||||
MenuHandler *fn;
|
||||
} MenuEntry;
|
||||
static const MenuEntry menu[];
|
||||
Platform::MenuBarRef mainMenu;
|
||||
void PopulateMainMenu();
|
||||
void PopulateRecentFiles();
|
||||
|
||||
Platform::KeyboardEvent AcceleratorForCommand(Command id);
|
||||
void ActivateCommand(Command id);
|
||||
|
||||
static void MenuView(Command id);
|
||||
static void MenuEdit(Command id);
|
||||
static void MenuRequest(Command id);
|
||||
@ -607,6 +558,25 @@ public:
|
||||
void PasteClipboard(Vector trans, double theta, double scale);
|
||||
static void MenuClipboard(Command id);
|
||||
|
||||
Platform::MenuRef openRecentMenu;
|
||||
Platform::MenuRef linkRecentMenu;
|
||||
|
||||
Platform::MenuItemRef showGridMenuItem;
|
||||
Platform::MenuItemRef perspectiveProjMenuItem;
|
||||
Platform::MenuItemRef showToolbarMenuItem;
|
||||
Platform::MenuItemRef showTextWndMenuItem;
|
||||
Platform::MenuItemRef fullScreenMenuItem;
|
||||
|
||||
Platform::MenuItemRef unitsMmMenuItem;
|
||||
Platform::MenuItemRef unitsMetersMenuItem;
|
||||
Platform::MenuItemRef unitsInchesMenuItem;
|
||||
|
||||
Platform::MenuItemRef inWorkplaneMenuItem;
|
||||
Platform::MenuItemRef in3dMenuItem;
|
||||
|
||||
Platform::MenuItemRef undoMenuItem;
|
||||
Platform::MenuItemRef redoMenuItem;
|
||||
|
||||
std::shared_ptr<ViewportCanvas> canvas;
|
||||
std::shared_ptr<BatchCanvas> persistentCanvas;
|
||||
bool persistentDirty;
|
||||
@ -829,9 +799,6 @@ public:
|
||||
void SelectByMarquee();
|
||||
void ClearSuper();
|
||||
|
||||
void ContextMenuListStyles();
|
||||
int64_t contextMenuCancelTime;
|
||||
|
||||
// The toolbar, in toolbar.cpp
|
||||
bool ToolbarDrawOrHitTest(int x, int y, UiCanvas *canvas, Command *menuHit);
|
||||
void ToolbarDraw(UiCanvas *canvas);
|
||||
@ -879,7 +846,7 @@ public:
|
||||
void MouseRightUp(double x, double y);
|
||||
void MouseScroll(double x, double y, int delta);
|
||||
void MouseLeave();
|
||||
bool KeyDown(int c);
|
||||
bool KeyboardEvent(Platform::KeyboardEvent event);
|
||||
void EditControlDone(const char *s);
|
||||
|
||||
int64_t lastSpaceNavigatorTime;
|
||||
|
@ -31,8 +31,8 @@ void SolveSpaceUI::UndoRedo() {
|
||||
}
|
||||
|
||||
void SolveSpaceUI::UndoEnableMenus() {
|
||||
EnableMenuByCmd(Command::UNDO, undo.cnt > 0);
|
||||
EnableMenuByCmd(Command::REDO, redo.cnt > 0);
|
||||
SS.GW.undoMenuItem->SetEnabled(undo.cnt > 0);
|
||||
SS.GW.redoMenuItem->SetEnabled(redo.cnt > 0);
|
||||
}
|
||||
|
||||
void SolveSpaceUI::PushFromCurrentOnto(UndoStack *uk) {
|
||||
|
Loading…
Reference in New Issue
Block a user