diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d9eaa37..071e9e8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index d46e86f..77a1f77 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.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 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 pathnames, + std::function 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); } } diff --git a/src/group.cpp b/src/group.cpp index 5ac130d..09a5568 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -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; diff --git a/src/mouse.cpp b/src/mouse.cpp index 3121b98..0ffc4ac 100644 --- a/src/mouse.cpp +++ b/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); diff --git a/src/platform/cocoamain.mm b/src/platform/cocoamain.mm index e67dc86..23bd12d 100644 --- a/src/platform/cocoamain.mm +++ b/src/platform/cocoamain.mm @@ -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 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::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"))]; } diff --git a/src/platform/gtkmain.cpp b/src/platform/gtkmain.cpp index 91d2f98..62d821b 100644 --- a/src/platform/gtkmain.cpp +++ b/src/platform/gtkmain.cpp @@ -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 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 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 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(localizedEntry); - break; - - case GraphicsWindow::MenuKind::CHECK: - menu_item = new MainMenuItem(localizedEntry); - break; - - case GraphicsWindow::MenuKind::RADIO: - MainMenuItem *radio_item = - new MainMenuItem(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(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*)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(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"))); } diff --git a/src/platform/gui.cpp b/src/platform/gui.cpp new file mode 100644 index 0000000..cbcca47 --- /dev/null +++ b/src/platform/gui.cpp @@ -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; +} + +} +} diff --git a/src/platform/gui.h b/src/platform/gui.h index ed93637..1faded9 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -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 TimerRef; TimerRef CreateTimer(); +// A native menu item. +class MenuItem { +public: + enum class Indicator { + NONE, + CHECK_MARK, + RADIO_MARK, + }; + + std::function 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 MenuItemRef; + +// A native menu. +class Menu { +public: + virtual ~Menu() {} + + virtual std::shared_ptr AddItem( + const std::string &label, std::function onTrigger = std::function()) = 0; + virtual std::shared_ptr AddSubMenu(const std::string &label) = 0; + virtual void AddSeparator() = 0; + + virtual void PopUp() = 0; + + virtual void Clear() = 0; +}; + +typedef std::shared_ptr MenuRef; + +// A native menu bar. +class MenuBar { +public: + virtual ~MenuBar() {} + + virtual std::shared_ptr AddSubMenu(const std::string &label) = 0; + + virtual void Clear() = 0; + virtual void *NativePtr() = 0; +}; + +typedef std::shared_ptr MenuBarRef; + +MenuRef CreateMenu(); +MenuBarRef GetOrCreateMainMenu(bool *unique); + } #endif diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp index 1df7f6d..8b13885 100644 --- a/src/platform/guigtk.cpp +++ b/src/platform/guigtk.cpp @@ -4,6 +4,10 @@ // Copyright 2018 whitequark //----------------------------------------------------------------------------- #include +#include +#include +#include +#include #include "solvespace.h" namespace SolveSpace { @@ -36,5 +40,224 @@ TimerRef CreateTimer() { return std::unique_ptr(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 &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> menuItems; + std::vector> subMenus; + + MenuItemRef AddItem(const std::string &label, + std::function onTrigger = NULL) override { + auto menuItem = std::make_shared(); + 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(); + menuItems.push_back(menuItem); + + auto subMenu = std::make_shared(); + 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 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(); +} + +class MenuBarImplGtk : public MenuBar { +public: + Gtk::MenuBar gtkMenuBar; + std::vector> subMenus; + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + 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(); +} + } } diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm index c0ecbd8..3c346b1 100644 --- a/src/platform/guimac.mm +++ b/src/platform/guimac.mm @@ -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 *)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(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> menuItems; + std::vector> subMenus; + + MenuImplCocoa() { + nsMenu = [[NSMenu alloc] initWithTitle:@""]; + [nsMenu setAutoenablesItems:NO]; + } + + MenuItemRef AddItem(const std::string &label, + std::function onTrigger = NULL) override { + auto menuItem = std::make_shared(); + 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(); + 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(); +} + +class MenuBarImplCocoa : public MenuBar { +public: + NSMenu *nsMenuBar; + + std::vector> subMenus; + + MenuBarImplCocoa() { + nsMenuBar = [NSApp mainMenu]; + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + 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 mainMenu; + if(!mainMenu) { + mainMenu = std::make_shared(); + } + *unique = true; + return mainMenu; +} + +void SetMainMenu(MenuBarRef menuBar) { +} + } } diff --git a/src/platform/guinone.cpp b/src/platform/guinone.cpp index 95e7fa1..416c86b 100644 --- a/src/platform/guinone.cpp +++ b/src/platform/guinone.cpp @@ -23,6 +23,54 @@ TimerRef CreateTimer() { return std::unique_ptr(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 onTrigger = NULL) override { + return std::make_shared(); + } + MenuRef AddSubMenu(const std::string &label) override { + return std::make_shared(); + } + + void AddSeparator() override {} + void PopUp() override {} + void Clear() override {} +}; + +MenuRef CreateMenu() { + return std::make_shared(); +} + +class MenuBarImplDummy : public MenuBar { +public: + MenuRef AddSubMenu(const std::string &label) override { + return std::make_shared(); + } + void Clear() override {} + void *NativePtr() override { return NULL; } +}; + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + *unique = false; + return std::make_shared(); +} + +} + +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 diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp index f23bfd8..85afafa 100644 --- a/src/platform/guiwin.cpp +++ b/src/platform/guiwin.cpp @@ -76,5 +76,216 @@ TimerRef CreateTimer() { return std::unique_ptr(new TimerImplWin32); } +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuImplWin32; + +class MenuItemImplWin32 : public MenuItem { +public: + std::shared_ptr 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 weakThis; + std::vector> menuItems; + std::vector> 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 onTrigger = NULL) override { + auto menuItem = std::make_shared(); + 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(); + 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(); + // 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> subMenus; + + MenuBarImplWin32() { + sscheck(hMenuBar = ::CreateMenu()); + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + 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(); +} + } } diff --git a/src/platform/w32main.cpp b/src/platform/w32main.cpp index 7478026..d18f4b0 100644 --- a/src/platform/w32main.cpp +++ b/src/platform/w32main.cpp @@ -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 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()); } diff --git a/src/resource.cpp b/src/resource.cpp index a976ce6..0f051f8 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -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; diff --git a/src/sketch.h b/src/sketch.h index 33a9895..1b88a08 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -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 diff --git a/src/solvespace.cpp b/src/solvespace.cpp index f1e32ac..3c11d01 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -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"); diff --git a/src/solvespace.h b/src/solvespace.h index 21d4fc9..a0dcffa 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -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 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 recentFiles; + bool Load(const Platform::Path &filename); bool GetFilenameAndSave(bool saveAs); bool OkayToStartNewFile(); hGroup CreateDefaultDrawingGroup(); diff --git a/src/toolbar.cpp b/src/toolbar.cpp index 5a2dfa4..fafc39c 100644 --- a/src/toolbar.cpp +++ b/src/toolbar.cpp @@ -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; } diff --git a/src/ui.h b/src/ui.h index 70c6175..339f6cd 100644 --- a/src/ui.h +++ b/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 canvas; std::shared_ptr 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; diff --git a/src/undoredo.cpp b/src/undoredo.cpp index 1739caf..111501e 100644 --- a/src/undoredo.cpp +++ b/src/undoredo.cpp @@ -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) {