diff --git a/CHANGELOG.md b/CHANGELOG.md index f97a4c90..c8c6f0ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ New measurement/analysis features: Other new features: * New command-line interface, for batch exporting and more. + * The graphical interface now supports HiDPI screens on every OS. * New link to match the on-screen size of the sketch with its actual size, "view → set to full scale". * When zooming to fit, constraints are also considered. diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index d35b5043..1a9e73d2 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -118,7 +118,7 @@ endfunction() # Second, register all resources. if(WIN32) add_resource(win32/icon.ico 4000 ICON) - add_resource(win32/manifest.xml 2 RT_MANIFEST) + add_resource(win32/manifest.xml 1 RT_MANIFEST) elseif(APPLE) add_iconset (cocoa/AppIcon.iconset) add_xib (cocoa/MainMenu.xib) diff --git a/res/win32/manifest.xml b/res/win32/manifest.xml index 82834364..7c49a2e3 100644 --- a/res/win32/manifest.xml +++ b/res/win32/manifest.xml @@ -1,17 +1,18 @@ - + - - - true - - Parametric 3d CAD tool. + + + PerMonitorV2, PerMonitor + true + + Use Perspective Projection.")); } - InvalidateGraphics(); + SS.GW.Invalidate(); break; } case Edit::GRID_SPACING: { SS.gridSpacing = (float)min(1e4, max(1e-3, SS.StringToMm(s))); - InvalidateGraphics(); + SS.GW.Invalidate(); break; } case Edit::DIGITS_AFTER_DECIMAL: { - int v = atoi(s); + int v = atoi(s.c_str()); if(v < 0 || v > 8) { Error(_("Specify between 0 and 8 digits after the decimal.")); } else { SS.SetUnitDigitsAfterDecimal(v); } - InvalidateGraphics(); + SS.GW.Invalidate(); break; } case Edit::EXPORT_SCALE: { @@ -455,8 +454,8 @@ bool TextWindow::EditControlDoneForConfiguration(const char *s) { break; } case Edit::AUTOSAVE_INTERVAL: { - int interval; - if(sscanf(s, "%d", &interval)==1) { + int interval = atoi(s.c_str()); + if(interval) { if(interval >= 1) { SS.autosaveInterval = interval; SS.ScheduleAutosave(); diff --git a/src/constraint.cpp b/src/constraint.cpp index 516b8b01..a48ef10e 100644 --- a/src/constraint.cpp +++ b/src/constraint.cpp @@ -744,7 +744,7 @@ void Constraint::MenuConstrain(Command id) { } SS.GW.ClearSelection(); - InvalidateGraphics(); + SS.GW.Invalidate(); } #endif /* ! LIBRARY */ diff --git a/src/draw.cpp b/src/draw.cpp index d79c9bfd..86ea59ed 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -80,7 +80,7 @@ void GraphicsWindow::Selection::Draw(bool isHovered, Canvas *canvas) { void GraphicsWindow::ClearSelection() { selection.Clear(); SS.ScheduleShowTW(); - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::ClearNonexistentSelectionItems() { @@ -98,7 +98,7 @@ void GraphicsWindow::ClearNonexistentSelectionItems() { } } selection.RemoveTagged(); - if(change) InvalidateGraphics(); + if(change) Invalidate(); } //----------------------------------------------------------------------------- @@ -304,14 +304,14 @@ void GraphicsWindow::GroupSelection() { Camera GraphicsWindow::GetCamera() const { Camera camera = {}; - camera.width = (int)width; - camera.height = (int)height; - camera.offset = offset; - camera.projUp = projUp; - camera.projRight = projRight; - camera.scale = scale; - camera.tangent = SS.CameraTangent(); - camera.hasPixels = true; + window->GetContentSize(&camera.width, &camera.height); + camera.pixelRatio = window->GetDevicePixelRatio(); + camera.gridFit = (window->GetDevicePixelRatio() == 1); + camera.offset = offset; + camera.projUp = projUp; + camera.projRight = projRight; + camera.scale = scale; + camera.tangent = SS.CameraTangent(); return camera; } @@ -457,7 +457,7 @@ void GraphicsWindow::HitTestMakeSelection(Point2d mp) { if(!sel.Equals(&hover)) { hover = sel; - InvalidateGraphics(); + Invalidate(); } } @@ -545,6 +545,10 @@ void GraphicsWindow::NormalizeProjectionVectors() { void GraphicsWindow::DrawSnapGrid(Canvas *canvas) { if(!LockedInWorkplane()) return; + const Camera &camera = canvas->GetCamera(); + double width = camera.width, + height = camera.height; + hEntity he = ActiveWorkplane(); EntityBase *wrkpl = SK.GetEntity(he), *norm = wrkpl->Normal(); @@ -816,15 +820,11 @@ void GraphicsWindow::Draw(Canvas *canvas) { } void GraphicsWindow::Paint() { - if(!canvas) return; + ssassert(window != NULL && canvas != NULL, + "Cannot paint without window and canvas"); havePainted = true; - int w, h; - GetGraphicsWindowSize(&w, &h); - width = w; - height = h; - Camera camera = GetCamera(); Lighting lighting = GetLighting(); @@ -902,3 +902,12 @@ void GraphicsWindow::Paint() { canvas->FlushFrame(); canvas->Clear(); } + +void GraphicsWindow::Invalidate(bool clearPersistent) { + if(window) { + if(clearPersistent) { + persistentDirty = true; + } + window->Invalidate(); + } +} diff --git a/src/export.cpp b/src/export.cpp index bafd6852..d68dce05 100644 --- a/src/export.cpp +++ b/src/export.cpp @@ -263,7 +263,7 @@ void SolveSpaceUI::ExportViewOrWireframeTo(const Platform::Path &filename, bool } SS.justExportedInfo.draw = true; - InvalidateGraphics(); + GW.Invalidate(); } edges.Clear(); @@ -839,7 +839,7 @@ void SolveSpaceUI::ExportMeshTo(const Platform::Path &filename) { SS.justExportedInfo.showOrigin = false; SS.justExportedInfo.draw = true; - InvalidateGraphics(); + GW.Invalidate(); } //----------------------------------------------------------------------------- @@ -1108,5 +1108,5 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename void SolveSpaceUI::ExportAsPngTo(const Platform::Path &filename) { screenshotFile = filename; // The rest of the work is done in the next redraw. - InvalidateGraphics(); + GW.Invalidate(); } diff --git a/src/expr.cpp b/src/expr.cpp index cbff0385..700d6b69 100644 --- a/src/expr.cpp +++ b/src/expr.cpp @@ -605,8 +605,7 @@ public: bool IsError() const { return type == TokenType::ERROR; } }; - const char *input; - unsigned inputPos; + std::string::const_iterator it, end; std::vector stack; char ReadChar(); @@ -624,7 +623,7 @@ public: bool Reduce(std::string *error); bool Parse(std::string *error, size_t reduceUntil = 0); - static Expr *Parse(const char *input, std::string *error); + static Expr *Parse(const std::string &input, std::string *error); }; ExprParser::Token ExprParser::Token::From(TokenType type, Expr *expr) { @@ -643,11 +642,15 @@ ExprParser::Token ExprParser::Token::From(TokenType type, Expr::Op op) { } char ExprParser::ReadChar() { - return input[inputPos++]; + return *it++; } char ExprParser::PeekChar() { - return input[inputPos]; + if(it == end) { + return '\0'; + } else { + return *it; + } } std::string ExprParser::ReadWord() { @@ -889,10 +892,10 @@ bool ExprParser::Parse(std::string *error, size_t reduceUntil) { return true; } -Expr *ExprParser::Parse(const char *input, std::string *error) { +Expr *ExprParser::Parse(const std::string &input, std::string *error) { ExprParser parser; - parser.input = input; - parser.inputPos = 0; + parser.it = input.cbegin(); + parser.end = input.cend(); if(!parser.Parse(error)) return NULL; Token r = parser.PopOperand(error); @@ -900,17 +903,18 @@ Expr *ExprParser::Parse(const char *input, std::string *error) { return r.expr; } -Expr *Expr::Parse(const char *input, std::string *error) { +Expr *Expr::Parse(const std::string &input, std::string *error) { return ExprParser::Parse(input, error); } -Expr *Expr::From(const char *input, bool popUpError) { +Expr *Expr::From(const std::string &input, bool popUpError) { std::string error; Expr *e = ExprParser::Parse(input, &error); if(!e) { dbp("Parse/lex error: %s", error.c_str()); if(popUpError) { - Error("Not a valid number or expression: '%s'.\n%s.", input, error.c_str()); + Error("Not a valid number or expression: '%s'.\n%s.", + input.c_str(), error.c_str()); } } return e; diff --git a/src/expr.h b/src/expr.h index 83ba23b3..7383d993 100644 --- a/src/expr.h +++ b/src/expr.h @@ -96,8 +96,8 @@ public: Expr *DeepCopyWithParamsAsPointers(IdList *firstTry, IdList *thenTry) const; - static Expr *Parse(const char *input, std::string *error); - static Expr *From(const char *in, bool popUpError); + static Expr *Parse(const std::string &input, std::string *error); + static Expr *From(const std::string &input, bool popUpError); }; class ExprVector { diff --git a/src/generate.cpp b/src/generate.cpp index 62f35fa2..ae37ba5d 100644 --- a/src/generate.cpp +++ b/src/generate.cpp @@ -313,7 +313,7 @@ void SolveSpaceUI::GenerateAll(Generate type, bool andFindFree, bool genForBBox) } prev.Clear(); - InvalidateGraphics(); + GW.Invalidate(); // Remove nonexistent selection items, for same reason we waited till // the end to put up a dialog box. diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index 77a1f773..6e67415d 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -255,7 +255,7 @@ bool GraphicsWindow::KeyboardEvent(Platform::KeyboardEvent event) { void GraphicsWindow::PopulateMainMenu() { bool unique = false; - mainMenu = Platform::GetOrCreateMainMenu(&unique); + Platform::MenuBarRef mainMenu = Platform::GetOrCreateMainMenu(&unique); if(unique) mainMenu->Clear(); Platform::MenuRef currentSubMenu; @@ -285,8 +285,9 @@ void GraphicsWindow::PopulateMainMenu() { SetLocale(locale.Culture()); CnfFreezeString(locale.Culture(), "Locale"); - SS.UpdateWindowTitle(); + SS.UpdateWindowTitles(); PopulateMainMenu(); + EnsureValidActives(); }); } } else if(Menu[i].fn == NULL) { @@ -331,7 +332,7 @@ void GraphicsWindow::PopulateMainMenu() { PopulateRecentFiles(); SS.UndoEnableMenus(); - SetMainMenu(mainMenu); + window->SetMenuBar(mainMenu); } static void PopulateMenuWithPathnames(Platform::MenuRef menu, @@ -361,15 +362,7 @@ void GraphicsWindow::PopulateRecentFiles() { } void GraphicsWindow::Init() { - PopulateMainMenu(); - - canvas = CreateRenderer(); - if(canvas) { - persistentCanvas = canvas->CreateBatch(); - persistentDirty = true; - } - - scale = 5; + scale = 5; offset = Vector::From(0, 0, 0); projRight = Vector::From(1, 0, 0); projUp = Vector::From(0, 1, 0); @@ -394,11 +387,30 @@ void GraphicsWindow::Init() { drawOccludedAs = DrawOccludedAs::INVISIBLE; showTextWindow = true; - ShowTextWindow(showTextWindow); showSnapGrid = false; context.active = false; + if(!window) { + window = Platform::CreateWindow(); + if(window) { + canvas = CreateRenderer(); + if(canvas) { + persistentCanvas = canvas->CreateBatch(); + persistentDirty = true; + } + + using namespace std::placeholders; + window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT); + window->onRender = std::bind(&GraphicsWindow::Paint, this); + window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1); + window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1); + window->onEditingDone = std::bind(&GraphicsWindow::EditControlDone, this, _1); + window->SetMinContentSize(720, 670); + PopulateMainMenu(); + } + } + // Do this last, so that all the menus get updated correctly. ClearSuper(); } @@ -444,7 +456,7 @@ void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) { projRight = quat.RotationU(); projUp = quat.RotationV(); - PaintGraphics(); + window->Redraw(); tn = GetMilliseconds(); s = (tn - t0)/((double)dt); @@ -453,16 +465,17 @@ void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) { projRight = quatf.RotationU(); projUp = quatf.RotationV(); offset = offsetf; - InvalidateGraphics(); + Invalidate(); // If the view screen is open, then we need to refresh it. SS.ScheduleShowTW(); } void GraphicsWindow::HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, - double *wmin, bool usePerspective) + double *wmin, bool usePerspective, + const Camera &camera) { double w; - Vector pp = ProjectPoint4(p, &w); + Vector pp = camera.ProjectPoint4(p, &w); // If usePerspective is true, then we calculate a perspective projection of the point. // If not, then we do a parallel projection regardless of the current // scale factor. @@ -480,11 +493,12 @@ void GraphicsWindow::LoopOverPoints(const std::vector &entities, const std::vector &constraints, const std::vector &faces, Point2d *pmax, Point2d *pmin, double *wmin, - bool usePerspective, bool includeMesh) { + bool usePerspective, bool includeMesh, + const Camera &camera) { for(Entity *e : entities) { if(e->IsPoint()) { - HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, usePerspective, camera); } else if(e->type == Entity::Type::CIRCLE) { // Lots of entities can extend outside the bbox of their points, // but circles are particularly bad. We want to get things halfway @@ -498,23 +512,23 @@ void GraphicsWindow::LoopOverPoints(const std::vector &entities, (j == 1) ? (c.Plus(q.RotationU().ScaledBy(-r))) : (j == 2) ? (c.Plus(q.RotationV().ScaledBy( r))) : (c.Plus(q.RotationV().ScaledBy(-r))); - HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); } } else { // We have to iterate children points, because we can select entities without points for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { if(e->point[i].v == 0) break; Vector p = SK.GetEntity(e->point[i])->PointGetNum(); - HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); } } } for(Constraint *c : constraints) { std::vector refs; - c->GetReferencePoints(GetCamera(), &refs); + c->GetReferencePoints(camera, &refs); for(Vector p : refs) { - HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); } } @@ -533,19 +547,25 @@ void GraphicsWindow::LoopOverPoints(const std::vector &entities, } if(!found) continue; } - HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective); - HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective); - HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective, camera); + HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective, camera); + HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective, camera); } if(!includeMesh) return; for(int i = 0; i < g->polyLoops.l.n; i++) { SContour *sc = &(g->polyLoops.l.elem[i]); for(int j = 0; j < sc->l.n; j++) { - HandlePointForZoomToFit(sc->l.elem[j].p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(sc->l.elem[j].p, pmax, pmin, wmin, usePerspective, camera); } } } void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { + if(!window) return; + + scale = ZoomToFit(GetCamera(), includingInvisibles, useSelection); +} +double GraphicsWindow::ZoomToFit(const Camera &camera, + bool includingInvisibles, bool useSelection) { std::vector entities; std::vector constraints; std::vector faces; @@ -588,7 +608,8 @@ void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { Point2d pmax = { -1e12, -1e12 }, pmin = { 1e12, 1e12 }; double wmin = 1; LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin, - /*usePerspective=*/false, /*includeMesh=*/!selectionUsed); + /*usePerspective=*/false, /*includeMesh=*/!selectionUsed, + camera); double xm = (pmax.x + pmin.x)/2, ym = (pmax.y + pmin.y)/2; double dx = pmax.x - pmin.x, dy = pmax.y - pmin.y; @@ -597,12 +618,13 @@ void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { projUp. ScaledBy(-ym)); // And based on this, we calculate the scale and offset + double scale; if(EXACT(dx == 0 && dy == 0)) { scale = 5; } else { double scalex = 1e12, scaley = 1e12; - if(EXACT(dx != 0)) scalex = 0.9*width /dx; - if(EXACT(dy != 0)) scaley = 0.9*height/dy; + if(EXACT(dx != 0)) scalex = 0.9*camera.width /dx; + if(EXACT(dy != 0)) scaley = 0.9*camera.height/dy; scale = min(scalex, scaley); scale = min(300.0, scale); @@ -614,17 +636,20 @@ void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { pmin.x = 1e12; pmin.y = 1e12; wmin = 1; LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin, - /*usePerspective=*/true, /*includeMesh=*/!selectionUsed); + /*usePerspective=*/true, /*includeMesh=*/!selectionUsed, + camera); // Adjust the scale so that no points are behind the camera if(wmin < 0.1) { - double k = SS.CameraTangent(); + double k = camera.tangent; // w = 1+k*scale*z double zmin = (wmin - 1)/(k*scale); // 0.1 = 1 + k*scale*zmin // (0.1 - 1)/(k*zmin) = scale scale = min(scale, (0.1 - 1)/(k*zmin)); } + + return scale; } void GraphicsWindow::MenuView(Command id) { @@ -650,7 +675,7 @@ void GraphicsWindow::MenuView(Command id) { Message(_("No workplane is active, so the grid will not appear.")); } SS.GW.EnsureValidActives(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::PERSPECTIVE_PROJ: @@ -663,7 +688,7 @@ void GraphicsWindow::MenuView(Command id) { "is typical.")); } SS.GW.EnsureValidActives(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::ONTO_WORKPLANE: @@ -745,7 +770,7 @@ void GraphicsWindow::MenuView(Command id) { case Command::SHOW_TOOLBAR: SS.showToolbar = !SS.showToolbar; SS.GW.EnsureValidActives(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::SHOW_TEXT_WND: @@ -772,13 +797,13 @@ void GraphicsWindow::MenuView(Command id) { break; case Command::FULL_SCREEN: - ToggleFullScreen(); + SS.GW.window->SetFullScreen(!SS.GW.window->IsFullScreen()); SS.GW.EnsureValidActives(); break; default: ssassert(false, "Unexpected menu ID"); } - InvalidateGraphics(); + SS.GW.Invalidate(); } void GraphicsWindow::EnsureValidActives() { @@ -827,6 +852,8 @@ void GraphicsWindow::EnsureValidActives() { } } + if(!window) return; + // And update the checked state for various menus bool locked = LockedInWorkplane(); in3dMenuItem->SetActive(!locked); @@ -847,13 +874,13 @@ void GraphicsWindow::EnsureValidActives() { unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS); unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES); - ShowTextWindow(SS.GW.showTextWindow); + if(SS.TW.window) SS.TW.window->SetVisible(SS.GW.showTextWindow); showTextWndMenuItem->SetActive(SS.GW.showTextWindow); showGridMenuItem->SetActive(SS.GW.showSnapGrid); perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj); showToolbarMenuItem->SetActive(SS.showToolbar); - fullScreenMenuItem->SetActive(FullScreenIsActive()); + fullScreenMenuItem->SetActive(SS.GW.window->IsFullScreen()); if(change) SS.ScheduleShowTW(); } @@ -877,7 +904,7 @@ void GraphicsWindow::ForceTextWindowShown() { if(!showTextWindow) { showTextWindow = true; showTextWndMenuItem->SetActive(true); - ShowTextWindow(true); + SS.TW.window->SetVisible(true); } } @@ -894,7 +921,7 @@ void GraphicsWindow::DeleteTaggedRequests() { // An edit might be in progress for the just-deleted item. So // now it's not. - HideGraphicsEditControl(); + window->HideEditor(); SS.TW.HideEditControl(); // And clear out the selection, which could contain that item. ClearSuper(); @@ -934,8 +961,8 @@ void GraphicsWindow::MenuEdit(Command id) { SS.GW.gs.constraints == 0 && SS.GW.pending.operation == Pending::NONE) { - if(!(TextEditControlIsVisible() || - GraphicsEditControlIsVisible())) + if(!(SS.TW.window->IsEditorVisible() || + SS.GW.window->IsEditorVisible())) { if(SS.TW.shown.screen == TextWindow::Screen::STYLE_INFO) { SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES); @@ -971,7 +998,7 @@ void GraphicsWindow::MenuEdit(Command id) { SS.GW.MakeSelected(e->h); } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); break; } @@ -1020,7 +1047,7 @@ void GraphicsWindow::MenuEdit(Command id) { if(newlySelected == 0) { Error(_("No additional entities share endpoints with the selected entities.")); } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); break; } @@ -1099,7 +1126,7 @@ void GraphicsWindow::MenuEdit(Command id) { SS.GW.ClearPending(); SS.GW.ClearSelection(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; } @@ -1156,7 +1183,7 @@ void GraphicsWindow::MenuRequest(Command id) { SS.GW.SetWorkplaneFreeIn3d(); SS.GW.EnsureValidActives(); SS.ScheduleShowTW(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::TANGENT_ARC: @@ -1171,7 +1198,7 @@ void GraphicsWindow::MenuRequest(Command id) { SS.TW.GoToScreen(TextWindow::Screen::TANGENT_ARC); SS.GW.ForceTextWindowShown(); SS.ScheduleShowTW(); - InvalidateGraphics(); // repaint toolbar + SS.GW.Invalidate(); // repaint toolbar } break; @@ -1196,7 +1223,7 @@ c: SS.GW.pending.command = id; SS.GW.pending.description = s; SS.ScheduleShowTW(); - InvalidateGraphics(); // repaint toolbar + SS.GW.Invalidate(); // repaint toolbar break; case Command::CONSTRUCTION: { @@ -1227,7 +1254,7 @@ c: } void GraphicsWindow::ClearSuper() { - HideGraphicsEditControl(); + if(window) window->HideEditor(); ClearPending(); ClearSelection(); hover.Clear(); @@ -1248,8 +1275,7 @@ void GraphicsWindow::ToggleBool(bool *v) { SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); } - SS.GW.persistentDirty = true; - InvalidateGraphics(); + Invalidate(/*clearPersistent=*/true); SS.ScheduleShowTW(); } diff --git a/src/mouse.cpp b/src/mouse.cpp index 0ffc4ac0..f823be85 100644 --- a/src/mouse.cpp +++ b/src/mouse.cpp @@ -84,7 +84,7 @@ void GraphicsWindow::StartDraggingBySelection() { void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, bool middleDown, bool rightDown, bool shiftDown, bool ctrlDown) { - if(GraphicsEditControlIsVisible()) return; + if(window->IsEditorVisible()) return; if(context.active) return; SS.extraLine.draw = false; @@ -112,7 +112,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, pending.operation == Pending::DRAGGING_MARQUEE)) { ClearPending(); - InvalidateGraphics(); + Invalidate(); } Point2d mp = Point2d::From(x, y); @@ -168,7 +168,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, SS.ScheduleShowTW(); } } - InvalidateGraphics(); + Invalidate(); havePainted = false; return; } @@ -282,7 +282,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, Constraint *c = SK.constraint.FindById(pending.constraint); UpdateDraggedNum(&(c->disp.offset), x, y); orig.mouse = mp; - InvalidateGraphics(); + Invalidate(); return; } @@ -298,7 +298,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, HitTestMakeSelection(mp); SS.MarkGroupDirtyByEntity(pending.point); orig.mouse = mp; - InvalidateGraphics(); + Invalidate(); break; case Pending::DRAGGING_POINTS: @@ -459,7 +459,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, case Pending::DRAGGING_MARQUEE: orig.mouse = mp; - InvalidateGraphics(); + Invalidate(); return; case Pending::NONE: @@ -497,7 +497,7 @@ void GraphicsWindow::ReplacePending(hRequest before, hRequest after) { } void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) { - if(GraphicsEditControlIsVisible()) return; + if(window->IsEditorVisible()) return; orig.offset = offset; orig.projUp = projUp; @@ -509,7 +509,7 @@ void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) { void GraphicsWindow::MouseRightUp(double x, double y) { SS.extraLine.draw = false; - InvalidateGraphics(); + Invalidate(); // Don't show a context menu if the user is right-clicking the toolbar, // or if they are finishing a pan. @@ -866,13 +866,67 @@ bool GraphicsWindow::ConstrainPointByHovered(hEntity pt, const Point2d *projecte return false; } +bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) { + using Platform::MouseEvent; + + double width, height; + window->GetContentSize(&width, &height); + + event.x = event.x - width / 2; + event.y = height / 2 - event.y; + + switch(event.type) { + case MouseEvent::Type::MOTION: + this->MouseMoved(event.x, event.y, + event.button == MouseEvent::Button::LEFT, + event.button == MouseEvent::Button::MIDDLE, + event.button == MouseEvent::Button::RIGHT, + event.shiftDown, + event.controlDown); + break; + + case MouseEvent::Type::PRESS: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftDown(event.x, event.y); + } else if(event.button == MouseEvent::Button::MIDDLE || + event.button == MouseEvent::Button::RIGHT) { + this->MouseMiddleOrRightDown(event.x, event.y); + } + break; + + case MouseEvent::Type::DBL_PRESS: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftDoubleClick(event.x, event.y); + } + break; + + case MouseEvent::Type::RELEASE: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftUp(event.x, event.y); + } else if(event.button == MouseEvent::Button::RIGHT) { + this->MouseRightUp(event.x, event.y); + } + break; + + case MouseEvent::Type::SCROLL_VERT: + this->MouseScroll(event.x, event.y, event.scrollDelta); + break; + + case MouseEvent::Type::LEAVE: + this->MouseLeave(); + break; + } + + return true; +} + void GraphicsWindow::MouseLeftDown(double mx, double my) { orig.mouseDown = true; - if(GraphicsEditControlIsVisible()) { + if(window->IsEditorVisible()) { orig.mouse = Point2d::From(mx, my); orig.mouseOnButtonDown = orig.mouse; - HideGraphicsEditControl(); + window->HideEditor(); return; } SS.TW.HideEditControl(); @@ -1234,7 +1288,7 @@ void GraphicsWindow::MouseLeftDown(double mx, double my) { } SS.ScheduleShowTW(); - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::MouseLeftUp(double mx, double my) { @@ -1249,13 +1303,13 @@ void GraphicsWindow::MouseLeftUp(double mx, double my) { case Pending::DRAGGING_NORMAL: case Pending::DRAGGING_RADIUS: ClearPending(); - InvalidateGraphics(); + Invalidate(); break; case Pending::DRAGGING_MARQUEE: SelectByMarquee(); ClearPending(); - InvalidateGraphics(); + Invalidate(); break; case Pending::NONE: @@ -1270,7 +1324,7 @@ void GraphicsWindow::MouseLeftUp(double mx, double my) { } void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { - if(GraphicsEditControlIsVisible()) return; + if(window->IsEditorVisible()) return; SS.TW.HideEditControl(); if(hover.constraint.v) { @@ -1291,17 +1345,17 @@ void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { Point2d p2 = ProjectPoint(p3); std::string editValue; - int editMinWidthChar; + std::string editPlaceholder; switch(c->type) { case Constraint::Type::COMMENT: editValue = c->comment; - editMinWidthChar = 30; + editPlaceholder = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; break; case Constraint::Type::ANGLE: case Constraint::Type::LENGTH_RATIO: editValue = ssprintf("%.3f", c->valA); - editMinWidthChar = 5; + editPlaceholder = "0.000"; break; default: { @@ -1327,20 +1381,28 @@ void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { if(fabs(std::stod(editValue) - v) < eps) break; } } - editMinWidthChar = 5; + editPlaceholder = "0.00000"; break; } } + + double width, height; + window->GetContentSize(&width, &height); hStyle hs = c->disp.style; if(hs.v == 0) hs.v = Style::CONSTRAINT; - ShowGraphicsEditControl((int)p2.x, (int)p2.y, - (int)(VectorFont::Builtin()->GetHeight(Style::TextHeight(hs))), - editMinWidthChar, editValue); + double capHeight = Style::TextHeight(hs); + double fontHeight = VectorFont::Builtin()->GetHeight(capHeight); + double editMinWidth = VectorFont::Builtin()->GetWidth(capHeight, editPlaceholder); + window->ShowEditor(p2.x + width / 2, height / 2 - p2.y, + fontHeight, editMinWidth, + /*isMonospace=*/false, editValue); } } -void GraphicsWindow::EditControlDone(const char *s) { - HideGraphicsEditControl(); +void GraphicsWindow::EditControlDone(const std::string &s) { + window->HideEditor(); + window->Invalidate(); + Constraint *c = SK.GetConstraint(constraintBeingEdited); if(c->type == Constraint::Type::COMMENT) { @@ -1349,8 +1411,7 @@ void GraphicsWindow::EditControlDone(const char *s) { return; } - Expr *e = Expr::From(s, true); - if(e) { + if(Expr *e = Expr::From(s, true)) { SS.UndoRemember(); switch(c->type) { @@ -1420,7 +1481,7 @@ void GraphicsWindow::MouseScroll(double x, double y, int delta) { } } havePainted = false; - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::MouseLeave() { @@ -1429,7 +1490,7 @@ void GraphicsWindow::MouseLeave() { if(!context.active) { hover.Clear(); toolbarHovered = Command::NONE; - PaintGraphics(); + Invalidate(); } SS.extraLine.draw = false; } @@ -1496,11 +1557,11 @@ void GraphicsWindow::SpaceNavigatorMoved(double tx, double ty, double tz, } havePainted = false; - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::SpaceNavigatorButtonUp() { ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true); - InvalidateGraphics(); + Invalidate(); } diff --git a/src/platform/climain.cpp b/src/platform/climain.cpp index 34d3fe36..5074de28 100644 --- a/src/platform/climain.cpp +++ b/src/platform/climain.cpp @@ -191,16 +191,32 @@ static bool RunCommand(const std::vector args) { } runner = [&](const Platform::Path &output) { - SS.GW.width = width; - SS.GW.height = height; - SS.GW.projRight = projRight; - SS.GW.projUp = projUp; - SS.chordTol = chordTol; + Camera camera = {}; + camera.pixelRatio = 1; + camera.gridFit = true; + camera.width = width; + camera.height = height; + camera.projUp = SS.GW.projUp; + camera.projRight = SS.GW.projRight; - SS.GW.ZoomToFit(/*includingInvisibles=*/false); + SS.GW.projUp = projUp; + SS.GW.projRight = projRight; + SS.GW.scale = SS.GW.ZoomToFit(camera); + camera.scale = SS.GW.scale; SS.GenerateAll(); - PaintGraphics(); - framebuffer->WritePng(output, /*flip=*/true); + + CairoPixmapRenderer *pixmapCanvas = (CairoPixmapRenderer *)SS.GW.canvas.get(); + pixmapCanvas->antialias = true; + pixmapCanvas->SetLighting(SS.GW.GetLighting()); + pixmapCanvas->SetCamera(camera); + pixmapCanvas->Init(); + + SS.GW.canvas->NewFrame(); + SS.GW.Draw(SS.GW.canvas.get()); + SS.GW.canvas->FlushFrame(); + SS.GW.canvas->ReadFrame()->WritePng(output, /*flip=*/true); + + pixmapCanvas->Clear(); }; } else if(args[1] == "export-view") { for(size_t argn = 2; argn < args.size(); argn++) { diff --git a/src/platform/cocoamain.mm b/src/platform/cocoamain.mm index 23bd12d8..77ef7da5 100644 --- a/src/platform/cocoamain.mm +++ b/src/platform/cocoamain.mm @@ -62,419 +62,6 @@ std::string CnfThawString(const std::string &val, const std::string &key) { } }; -/* OpenGL view */ - -@interface GLViewWithEditor : NSView -- (void)drawGL; - -@property BOOL wantsBackingStoreScaling; - -@property(readonly, getter=isEditing) BOOL editing; -- (void)startEditing:(NSString*)text at:(NSPoint)origin withHeight:(double)fontHeight - usingMonospace:(BOOL)isMonospace; -- (void)stopEditing; -- (void)didEdit:(NSString*)text; -@end - -@implementation GLViewWithEditor -{ - SolveSpace::GlOffscreen offscreen; - NSOpenGLContext *glContext; -@protected - NSTextField *editor; -} - -- (id)initWithFrame:(NSRect)frameRect { - self = [super initWithFrame:frameRect]; - [self setWantsLayer:YES]; - - NSOpenGLPixelFormatAttribute attrs[] = { - NSOpenGLPFAColorSize, 24, - NSOpenGLPFADepthSize, 24, - 0 - }; - NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; - glContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:NULL]; - - editor = [[NSTextField alloc] init]; - [editor setEditable:YES]; - [[editor cell] setWraps:NO]; - [[editor cell] setScrollable:YES]; - [editor setBezeled:NO]; - [editor setTarget:self]; - [editor setAction:@selector(editorAction:)]; - - return self; -} - -- (void)dealloc { - offscreen.Clear(); -} - -#define CONVERT1(name, to_from) \ - - (NS##name)convert##name##to_from##Backing:(NS##name)input { \ - return _wantsBackingStoreScaling ? [super convert##name##to_from##Backing:input] : input; } -#define CONVERT(name) CONVERT1(name, To) CONVERT1(name, From) -CONVERT(Size) -CONVERT(Rect) -#undef CONVERT -#undef CONVERT1 - -- (NSPoint)convertPointToBacking:(NSPoint)input { - if(_wantsBackingStoreScaling) return [super convertPointToBacking:input]; - else { - input.y *= -1; - return input; - } -} - -- (NSPoint)convertPointFromBacking:(NSPoint)input { - if(_wantsBackingStoreScaling) return [super convertPointFromBacking:input]; - else { - input.y *= -1; - return input; - } -} - -- (void)drawRect:(NSRect)aRect { - [glContext makeCurrentContext]; - - NSSize size = [self convertSizeToBacking:[self bounds].size]; - int width = (int)size.width, - height = (int)size.height; - offscreen.Render(width, height, [&] { [self drawGL]; }); - - CGDataProviderRef provider = CGDataProviderCreateWithData( - NULL, &offscreen.data[0], width * height * 4, NULL); - CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); - CGImageRef image = CGImageCreate(width, height, 8, 32, - width * 4, colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, - provider, NULL, true, kCGRenderingIntentDefault); - - CGContextDrawImage((CGContextRef) [[NSGraphicsContext currentContext] graphicsPort], - [self bounds], image); - - CGImageRelease(image); - CGDataProviderRelease(provider); -} - -- (void)drawGL { -} - -@synthesize editing; - -- (void)startEditing:(NSString*)text at:(NSPoint)origin withHeight:(double)fontHeight - usingMonospace:(BOOL)isMonospace { - if(!self->editing) { - [self addSubview:editor]; - self->editing = YES; - } - - NSFont *font; - if(isMonospace) - font = [NSFont fontWithName:@"Monaco" size:fontHeight]; - else - font = [NSFont controlContentFontOfSize:fontHeight]; - [editor setFont:font]; - - origin.x -= 3; /* left padding; no way to get it from NSTextField */ - origin.y -= [editor intrinsicContentSize].height; - origin.y += [editor baselineOffsetFromBottom]; - - [editor setFrameOrigin:origin]; - [editor setStringValue:text]; - [[self window] makeFirstResponder:editor]; -} - -- (void)stopEditing { - if(self->editing) { - [editor removeFromSuperview]; - self->editing = NO; - } -} - -- (void)editorAction:(id)sender { - [self didEdit:[editor stringValue]]; - [self stopEditing]; -} - -- (void)didEdit:(NSString*)text { -} -@end - -/* Graphics window */ - -@interface GraphicsWindowView : GLViewWithEditor -{ - NSTrackingArea *trackingArea; -} - -@property(readonly) NSEvent *lastContextMenuEvent; -@end - -@implementation GraphicsWindowView -- (BOOL)isFlipped { - return YES; -} - -- (void)drawGL { - SolveSpace::SS.GW.Paint(); -} - -- (BOOL)acceptsFirstResponder { - return YES; -} - -- (void) createTrackingArea { - trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] - options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveInKeyWindow) - owner:self userInfo:nil]; - [self addTrackingArea:trackingArea]; -} - -- (void) updateTrackingAreas -{ - [self removeTrackingArea:trackingArea]; - [self createTrackingArea]; - [super updateTrackingAreas]; -} - -- (void)mouseMoved:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - NSUInteger flags = [event modifierFlags]; - NSUInteger buttons = [NSEvent pressedMouseButtons]; - SolveSpace::SS.GW.MouseMoved(point.x, point.y, - buttons & (1 << 0), - buttons & (1 << 2), - buttons & (1 << 1), - flags & NSShiftKeyMask, - flags & NSCommandKeyMask); -} - -- (void)mouseDragged:(NSEvent*)event { - [self mouseMoved:event]; -} - -- (void)rightMouseDragged:(NSEvent*)event { - [self mouseMoved:event]; -} - -- (void)otherMouseDragged:(NSEvent*)event { - [self mouseMoved:event]; -} - -- (void)mouseDown:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - if([event clickCount] == 1) - SolveSpace::SS.GW.MouseLeftDown(point.x, point.y); - else if([event clickCount] == 2) - SolveSpace::SS.GW.MouseLeftDoubleClick(point.x, point.y); -} - -- (void)rightMouseDown:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.GW.MouseMiddleOrRightDown(point.x, point.y); -} - -- (void)otherMouseDown:(NSEvent*)event { - [self rightMouseDown:event]; -} - -- (void)mouseUp:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.GW.MouseLeftUp(point.x, point.y); -} - -- (void)rightMouseUp:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - self->_lastContextMenuEvent = event; - SolveSpace::SS.GW.MouseRightUp(point.x, point.y); -} - -- (void)scrollWheel:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.GW.MouseScroll(point.x, point.y, (int)-[event deltaY]); -} - -- (void)mouseExited:(NSEvent*)event { - SolveSpace::SS.GW.MouseLeave(); -} - -- (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) { - event.key = KeyboardEvent::Key::FUNCTION; - event.num = chr - NSF1FunctionKey + 1; - } else { - event.key = KeyboardEvent::Key::CHARACTER; - event.chr = chr; - } - - if(SolveSpace::SS.GW.KeyboardEvent(event)) - return; - - [super keyDown:nsEvent]; -} - -- (void)startEditing:(NSString*)text at:(NSPoint)xy withHeight:(double)fontHeight - withMinWidthInChars:(int)minWidthChars { - // Convert to ij (vs. xy) style coordinates - NSSize size = [self convertSizeToBacking:[self bounds].size]; - NSPoint point = { - .x = xy.x + size.width / 2, - .y = xy.y - size.height / 2 - }; - [[self window] makeKeyWindow]; - [super startEditing:text at:[self convertPointFromBacking:point] - withHeight:fontHeight usingMonospace:FALSE]; - [self prepareEditorWithMinWidthInChars:minWidthChars]; -} - -- (void)prepareEditorWithMinWidthInChars:(int)minWidthChars { - NSFont *font = [editor font]; - NSGlyph glyphA = [font glyphWithName:@"a"]; - ssassert(glyphA != (NSGlyph)-1, "Expected font to have U+0061"); - CGFloat glyphAWidth = [font advancementForGlyph:glyphA].width; - - [editor sizeToFit]; - - NSSize frameSize = [editor frame].size; - frameSize.width = std::max(frameSize.width, glyphAWidth * minWidthChars); - [editor setFrameSize:frameSize]; -} - -- (void)didEdit:(NSString*)text { - SolveSpace::SS.GW.EditControlDone([text UTF8String]); - [self setNeedsDisplay:YES]; -} - -- (void)cancelOperation:(id)sender { - [self stopEditing]; -} - -- (NSPoint)ij_to_xy:(NSPoint)ij { - // Convert to xy (vs. ij) style coordinates, - // with (0, 0) at center - NSSize size = [self bounds].size; - return [self convertPointToBacking:(NSPoint){ - .x = ij.x - size.width / 2, .y = ij.y - size.height / 2 }]; -} -@end - -@interface GraphicsWindowDelegate : NSObject -- (BOOL)windowShouldClose:(id)sender; - -@property(readonly, getter=isFullscreen) BOOL fullscreen; -- (void)windowDidEnterFullScreen:(NSNotification *)notification; -- (void)windowDidExitFullScreen:(NSNotification *)notification; -@end - -@implementation GraphicsWindowDelegate -- (BOOL)windowShouldClose:(id)sender { - [NSApp terminate:sender]; - return FALSE; /* in case NSApp changes its mind */ -} - -@synthesize fullscreen; -- (void)windowDidEnterFullScreen:(NSNotification *)notification { - fullscreen = true; - /* Update the menus */ - SolveSpace::SS.GW.EnsureValidActives(); -} -- (void)windowDidExitFullScreen:(NSNotification *)notification { - fullscreen = false; - /* Update the menus */ - SolveSpace::SS.GW.EnsureValidActives(); -} -@end - -static NSWindow *GW; -static GraphicsWindowView *GWView; -static GraphicsWindowDelegate *GWDelegate; - -namespace SolveSpace { -void InitGraphicsWindow() { - GW = [[NSWindow alloc] init]; - GWDelegate = [[GraphicsWindowDelegate alloc] init]; - [GW setDelegate:GWDelegate]; - [GW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask | NSResizableWindowMask)]; - [GW setFrameAutosaveName:@"GraphicsWindow"]; - [GW setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - if(![GW setFrameUsingName:[GW frameAutosaveName]]) - [GW setContentSize:(NSSize){ .width = 600, .height = 600 }]; - GWView = [[GraphicsWindowView alloc] init]; - [GW setContentView:GWView]; -} - -void GetGraphicsWindowSize(int *w, int *h) { - NSSize size = [GWView convertSizeToBacking:[GWView frame].size]; - *w = (int)size.width; - *h = (int)size.height; -} - -void InvalidateGraphics() { - [GWView setNeedsDisplay:YES]; -} - -void PaintGraphics() { - [GWView setNeedsDisplay:YES]; - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); -} - -void SetCurrentFilename(const Platform::Path &filename) { - if(!filename.IsEmpty()) { - [GW setTitleWithRepresentedFilename:Wrap(filename.raw)]; - } else { - [GW setTitle:Wrap(C_("title", "(new sketch)"))]; - [GW setRepresentedFilename:@""]; - } -} - -void ToggleFullScreen() { - [GW toggleFullScreen:nil]; -} - -bool FullScreenIsActive() { - return [GWDelegate isFullscreen]; -} - -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &str) { - [GWView startEditing:Wrap(str) - at:(NSPoint){(CGFloat)x, (CGFloat)y} - withHeight:fontHeight - withMinWidthInChars:minWidthChars]; -} - -void HideGraphicsEditControl() { - [GWView stopEditing]; -} - -bool GraphicsEditControlIsVisible() { - return [GWView isEditing]; -} -} - /* Save/load */ bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension, @@ -652,207 +239,6 @@ SolveSpace::DialogChoice SolveSpace::LocateImportedFileYesNoCancel( } } -/* Text window */ - -@interface TextWindowView : GLViewWithEditor -{ - NSTrackingArea *trackingArea; -} - -@property (nonatomic, getter=isCursorHand) BOOL cursorHand; -@end - -@implementation TextWindowView -- (BOOL)isFlipped { - return YES; -} - -- (void)drawGL { - SolveSpace::SS.TW.Paint(); -} - -- (BOOL)acceptsFirstMouse:(NSEvent*)event { - return YES; -} - -- (void) createTrackingArea { - trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] - options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveAlways) - owner:self userInfo:nil]; - [self addTrackingArea:trackingArea]; -} - -- (void) updateTrackingAreas -{ - [self removeTrackingArea:trackingArea]; - [self createTrackingArea]; - [super updateTrackingAreas]; -} - -- (void)mouseMoved:(NSEvent*)event { - NSPoint point = [self convertPointToBacking: - [self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ false, - point.x, -point.y); -} - -- (void)mouseDown:(NSEvent*)event { - NSPoint point = [self convertPointToBacking: - [self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.TW.MouseEvent(/*leftClick*/ true, /*leftDown*/ true, - point.x, -point.y); -} - -- (void)mouseDragged:(NSEvent*)event { - NSPoint point = [self convertPointToBacking: - [self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ true, - point.x, -point.y); -} - -- (void)setCursorHand:(BOOL)cursorHand { - if(_cursorHand != cursorHand) { - if(cursorHand) - [[NSCursor pointingHandCursor] push]; - else - [NSCursor pop]; - } - _cursorHand = cursorHand; -} - -- (void)mouseExited:(NSEvent*)event { - [self setCursorHand:FALSE]; - SolveSpace::SS.TW.MouseLeave(); -} - -- (void)startEditing:(NSString*)text at:(NSPoint)point { - point = [self convertPointFromBacking:point]; - point.y = -point.y + 2; - [[self window] makeKeyWindow]; - [super startEditing:text at:point withHeight:15.0 usingMonospace:TRUE]; - [editor setFrameSize:(NSSize){ - .width = [self bounds].size.width - [editor frame].origin.x, - .height = [editor intrinsicContentSize].height }]; -} - -- (void)stopEditing { - [super stopEditing]; - [GW makeKeyWindow]; -} - -- (void)didEdit:(NSString*)text { - SolveSpace::SS.TW.EditControlDone([text UTF8String]); -} - -- (void)cancelOperation:(id)sender { - [self stopEditing]; -} -@end - -@interface TextWindowDelegate : NSObject -- (BOOL)windowShouldClose:(id)sender; -- (void)windowDidResize:(NSNotification *)notification; -@end - -@implementation TextWindowDelegate -- (BOOL)windowShouldClose:(id)sender { - SolveSpace::GraphicsWindow::MenuView(SolveSpace::Command::SHOW_TEXT_WND); - return NO; -} - -- (void)windowDidResize:(NSNotification *)notification { - NSClipView *view = [[[notification object] contentView] contentView]; - NSView *document = [view documentView]; - NSSize size = [document frame].size; - size.width = [view frame].size.width; - [document setFrameSize:size]; -} -@end - -static NSPanel *TW; -static TextWindowView *TWView; -static TextWindowDelegate *TWDelegate; - -namespace SolveSpace { -void InitTextWindow() { - TW = [[NSPanel alloc] init]; - TWDelegate = [[TextWindowDelegate alloc] init]; - [TW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | - NSUtilityWindowMask)]; - [[TW standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; - [[TW standardWindowButton:NSWindowZoomButton] setHidden:YES]; - [TW setFrameAutosaveName:@"TextWindow"]; - [TW setFloatingPanel:YES]; - [TW setBecomesKeyOnlyIfNeeded:YES]; - - NSScrollView *scrollView = [[NSScrollView alloc] init]; - [TW setContentView:scrollView]; - [scrollView setBackgroundColor:[NSColor blackColor]]; - [scrollView setHasVerticalScroller:YES]; - [scrollView setScrollerKnobStyle:NSScrollerKnobStyleLight]; - [[scrollView contentView] setCopiesOnScroll:YES]; - - TWView = [[TextWindowView alloc] init]; - [scrollView setDocumentView:TWView]; - - [TW setDelegate:TWDelegate]; - if(![TW setFrameUsingName:[TW frameAutosaveName]]) - [TW setContentSize:(NSSize){ .width = 420, .height = 300 }]; - [TWView setFrame:[[scrollView contentView] frame]]; -} - -void ShowTextWindow(bool visible) { - if(visible) - [TW orderFront:nil]; - else - [TW close]; -} - -void GetTextWindowSize(int *w, int *h) { - NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; - *w = (int)size.width; - *h = (int)size.height; -} - -double GetScreenDpi() { - NSScreen *screen = [NSScreen mainScreen]; - NSDictionary *description = [screen deviceDescription]; - NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; - CGSize displayPhysicalSize = CGDisplayScreenSize( - [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); - return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f; -} - -void InvalidateText() { - NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; - size.height = (SS.TW.top[SS.TW.rows - 1] + 1) * TextWindow::LINE_HEIGHT / 2; - [TWView setFrameSize:[TWView convertSizeFromBacking:size]]; - [TWView setNeedsDisplay:YES]; -} - -void MoveTextScrollbarTo(int pos, int maxPos, int page) { - /* unused; we draw the entire text window and scroll in Cocoa */ -} - -void SetMousePointerToHand(bool is_hand) { - [TWView setCursorHand:is_hand]; -} - -void ShowTextEditControl(int x, int y, const std::string &str) { - return [TWView startEditing:Wrap(str) - at:(NSPoint){(CGFloat)x, (CGFloat)y}]; -} - -void HideTextEditControl() { - return [TWView stopEditing]; -} - -bool TextEditControlIsVisible() { - return [TWView isEditing]; -} -}; - /* Miscellanea */ void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { @@ -904,27 +290,15 @@ std::vector SolveSpace::GetFontFiles() { /* Application lifecycle */ @interface ApplicationDelegate : NSObject -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication; -- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; -- (void)applicationWillTerminate:(NSNotification *)aNotification; -- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; - (IBAction)preferences:(id)sender; +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; @end @implementation ApplicationDelegate -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication { - return YES; -} - -- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - if(SolveSpace::SS.OkayToStartNewFile()) - return NSTerminateNow; - else - return NSTerminateCancel; -} - -- (void)applicationWillTerminate:(NSNotification *)aNotification { - SolveSpace::SS.Exit(); +- (IBAction)preferences:(id)sender { + SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::Screen::CONFIGURATION); + SolveSpace::SS.ScheduleShowTW(); } - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { @@ -932,21 +306,16 @@ std::vector SolveSpace::GetFontFiles() { return SolveSpace::SS.Load(path.Expand(/*fromCurrentDirectory=*/true)); } -- (IBAction)preferences:(id)sender { - SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::Screen::CONFIGURATION); - SolveSpace::SS.ScheduleShowTW(); +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + [[[NSApp mainWindow] delegate] windowShouldClose:nil]; + return NSTerminateCancel; +} + +- (void)applicationTerminatePrompt { + SolveSpace::SS.MenuFile(SolveSpace::Command::EXIT); } @end -void SolveSpace::SetMainMenu(Platform::MenuBarRef menuBar) { - SS.UpdateWindowTitle(); - [TW setTitle:Wrap(C_("title", "Property Browser"))]; -} - -void SolveSpace::ExitNow() { - [NSApp stop:nil]; -} - /* * Normally we would just link to the 3DconnexionClient framework. * We don't want to (are not allowed to) distribute the official @@ -1097,17 +466,13 @@ static void connexionClose() { } int main(int argc, const char *argv[]) { - [NSApplication sharedApplication]; ApplicationDelegate *delegate = [[ApplicationDelegate alloc] init]; - [NSApp setDelegate:delegate]; + [[NSApplication sharedApplication] setDelegate:delegate]; - SolveSpace::InitGraphicsWindow(); - SolveSpace::InitTextWindow(); [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:nil topLevelObjects:nil]; NSArray *languages = [NSLocale preferredLanguages]; for(NSString *language in languages) { - dbp("%s", ([language UTF8String])); if(SolveSpace::SetLocale([language UTF8String])) break; } if([languages count] == 0) { @@ -1117,7 +482,6 @@ int main(int argc, const char *argv[]) { connexionInit(); SolveSpace::SS.Init(); - [GW makeKeyAndOrderFront:nil]; [NSApp run]; connexionClose(); diff --git a/src/platform/gtkmain.cpp b/src/platform/gtkmain.cpp index 62d821b3..2ab8e93d 100644 --- a/src/platform/gtkmain.cpp +++ b/src/platform/gtkmain.cpp @@ -162,439 +162,6 @@ std::string CnfThawString(const std::string &val, const std::string &key) { return val; } -static void CnfFreezeWindowPos(Gtk::Window *win, const std::string &key) { - int x, y, w, h; - win->get_position(x, y); - win->get_size(w, h); - - CnfFreezeInt(x, key + "_left"); - CnfFreezeInt(y, key + "_top"); - CnfFreezeInt(w, key + "_width"); - CnfFreezeInt(h, key + "_height"); -} - -static void CnfThawWindowPos(Gtk::Window *win, const std::string &key) { - int x, y, w, h; - win->get_position(x, y); - win->get_size(w, h); - - x = CnfThawInt(x, key + "_left"); - y = CnfThawInt(y, key + "_top"); - w = CnfThawInt(w, key + "_width"); - h = CnfThawInt(h, key + "_height"); - - win->move(x, y); - win->resize(w, h); -} - -/* Editor overlay */ - -class EditorOverlay : public Gtk::Fixed { -public: - EditorOverlay(Gtk::Widget &underlay) : _underlay(underlay) { - set_size_request(0, 0); - - add(_underlay); - - _entry.set_no_show_all(true); - _entry.set_has_frame(false); - add(_entry); - - _entry.signal_activate(). - connect(sigc::mem_fun(this, &EditorOverlay::on_activate)); - } - - void start_editing(int x, int y, int font_height, bool is_monospace, int minWidthChars, - const std::string &val) { - x /= get_scale_factor(); - y /= get_scale_factor(); - font_height /= get_scale_factor(); - - Pango::FontDescription font_desc; - font_desc.set_family(is_monospace ? "monospace" : "normal"); - font_desc.set_absolute_size(font_height * Pango::SCALE); - _entry.override_font(font_desc); - - /* y coordinate denotes baseline */ - Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc); - y -= font_metrics.get_ascent() / Pango::SCALE; - - Glib::RefPtr layout = Pango::Layout::create(get_pango_context()); - layout->set_font_description(font_desc); - layout->set_text(val + " "); /* avoid scrolling */ - int width = layout->get_logical_extents().get_width(); - - Gtk::Border margin = _entry.get_style_context()->get_margin(); - Gtk::Border border = _entry.get_style_context()->get_border(); - Gtk::Border padding = _entry.get_style_context()->get_padding(); - move(_entry, - x - margin.get_left() - border.get_left() - padding.get_left(), - y - margin.get_top() - border.get_top() - padding.get_top()); - _entry.set_width_chars(minWidthChars); - _entry.set_size_request( - width / Pango::SCALE + padding.get_left() + padding.get_right(), - -1); - - _entry.set_text(val); - if(!_entry.is_visible()) { - _entry.show(); - _entry.grab_focus(); - add_modal_grab(); - } - } - - void stop_editing() { - if(_entry.is_visible()) { - remove_modal_grab(); - } - _entry.hide(); - } - - bool is_editing() const { - return _entry.is_visible(); - } - - sigc::signal signal_editing_done() { - return _signal_editing_done; - } - - Gtk::Entry &get_entry() { - return _entry; - } - -protected: - bool on_key_press_event(GdkEventKey *event) override { - if(is_editing()) { - if(event->keyval == GDK_KEY_Escape) { - stop_editing(); - } else { - _entry.event((GdkEvent *)event); - } - return true; - } else { - return Gtk::Fixed::on_key_press_event(event); - } - } - - bool on_key_release_event(GdkEventKey *event) override { - if(is_editing()) { - _entry.event((GdkEvent *)event); - return true; - } else { - return Gtk::Fixed::on_key_release_event(event); - } - } - - void on_size_allocate(Gtk::Allocation& allocation) override { - Gtk::Fixed::on_size_allocate(allocation); - - _underlay.size_allocate(allocation); - } - - void on_activate() { - _signal_editing_done(_entry.get_text()); - } - -private: - Gtk::Widget &_underlay; - Gtk::Entry _entry; - sigc::signal _signal_editing_done; -}; - -/* Graphics window */ - -double DeltaYOfScrollEvent(GdkEventScroll *event) { - double delta_y = event->delta_y; - if(delta_y == 0) { - switch(event->direction) { - case GDK_SCROLL_UP: - delta_y = -1; - break; - - case GDK_SCROLL_DOWN: - delta_y = 1; - break; - - default: - /* do nothing */ - return false; - } - } - - return delta_y; -} - -class GraphicsWidget : public Gtk::GLArea { -public: - GraphicsWidget() { - set_events(Gdk::POINTER_MOTION_MASK | - Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | - Gdk::SCROLL_MASK | - Gdk::LEAVE_NOTIFY_MASK); - set_has_depth_buffer(true); - } - -protected: - // Work around a bug fixed in GTKMM 3.22: - // https://mail.gnome.org/archives/gtkmm-list/2016-April/msg00020.html - Glib::RefPtr on_create_context() override { - return get_window()->create_gl_context(); - } - - void on_resize(int width, int height) override { - _w = width; - _h = height; - } - - bool on_render(const Glib::RefPtr &context) override { - SS.GW.Paint(); - return true; - } - - bool on_motion_notify_event(GdkEventMotion *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - SS.GW.MouseMoved(x, y, - event->state & GDK_BUTTON1_MASK, - event->state & GDK_BUTTON2_MASK, - event->state & GDK_BUTTON3_MASK, - event->state & GDK_SHIFT_MASK, - event->state & GDK_CONTROL_MASK); - - return true; - } - - bool on_button_press_event(GdkEventButton *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - switch(event->button) { - case 1: - if(event->type == GDK_BUTTON_PRESS) - SS.GW.MouseLeftDown(x, y); - else if(event->type == GDK_2BUTTON_PRESS) - SS.GW.MouseLeftDoubleClick(x, y); - break; - - case 2: - case 3: - SS.GW.MouseMiddleOrRightDown(x, y); - break; - } - - return true; - } - - bool on_button_release_event(GdkEventButton *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - switch(event->button) { - case 1: - SS.GW.MouseLeftUp(x, y); - break; - - case 3: - SS.GW.MouseRightUp(x, y); - break; - } - - return true; - } - - bool on_scroll_event(GdkEventScroll *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - SS.GW.MouseScroll(x, y, (int)-DeltaYOfScrollEvent(event)); - - return true; - } - - bool on_leave_notify_event (GdkEventCrossing *) override { - SS.GW.MouseLeave(); - - return true; - } - -private: - int _w, _h; - void ij_to_xy(double i, double j, int &x, int &y) { - // Convert to xy (vs. ij) style coordinates, - // with (0, 0) at center - x = (int)(i * get_scale_factor()) - _w / 2; - y = _h / 2 - (int)(j * get_scale_factor()); - } -}; - -class GraphicsWindowGtk : public Gtk::Window { -public: - GraphicsWindowGtk() : _overlay(_widget), _is_fullscreen(false) { - set_default_size(900, 600); - - _box.pack_end(_overlay, true, true); - - add(_box); - - _overlay.signal_editing_done(). - connect(sigc::mem_fun(this, &GraphicsWindowGtk::on_editing_done)); - } - - GraphicsWidget &get_widget() { - return _widget; - } - - EditorOverlay &get_overlay() { - return _overlay; - } - - 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; - } - - bool is_fullscreen() const { - return _is_fullscreen; - } - -protected: - void on_show() override { - Gtk::Window::on_show(); - - CnfThawWindowPos(this, "GraphicsWindow"); - } - - void on_hide() override { - CnfFreezeWindowPos(this, "GraphicsWindow"); - - Gtk::Window::on_hide(); - } - - bool on_delete_event(GdkEventAny *) override { - if(!SS.OkayToStartNewFile()) return true; - SS.Exit(); - - return true; - } - - bool on_window_state_event(GdkEventWindowState *event) override { - _is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; - - /* The event arrives too late for the caller of ToggleFullScreen - to notice state change; and it's possible that the WM will - refuse our request, so we can't just toggle the saved state */ - SS.GW.EnsureValidActives(); - - return Gtk::Window::on_window_state_event(event); - } - - bool on_key_press_event(GdkEventKey *gdk_event) override { - Platform::KeyboardEvent event = {}; - event.type = Platform::KeyboardEvent::Type::PRESS; - - if(gdk_event->state & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { - return Gtk::Window::on_key_press_event(gdk_event); - } - - 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(SS.GW.KeyboardEvent(event)) { - return true; - } - - return Gtk::Window::on_key_press_event(gdk_event); - } - - void on_editing_done(Glib::ustring value) { - SS.GW.EditControlDone(value.c_str()); - } - -private: - GraphicsWidget _widget; - EditorOverlay _overlay; - Gtk::MenuBar *_menubar; - Gtk::VBox _box; - - bool _is_fullscreen; -}; - -std::unique_ptr GW; - -void GetGraphicsWindowSize(int *w, int *h) { - Gdk::Rectangle allocation = GW->get_widget().get_allocation(); - *w = allocation.get_width() * GW->get_scale_factor(); - *h = allocation.get_height() * GW->get_scale_factor(); -} - -void InvalidateGraphics(void) { - GW->get_widget().queue_draw(); -} - -void PaintGraphics(void) { - GW->get_widget().queue_draw(); - /* Process animation */ - Glib::MainContext::get_default()->iteration(false); -} - -void SetCurrentFilename(const Platform::Path &filename) { - GW->set_title(Title(filename.IsEmpty() ? C_("title", "(new sketch)") : filename.raw.c_str())); -} - -void ToggleFullScreen(void) { - if(GW->is_fullscreen()) - GW->unfullscreen(); - else - GW->fullscreen(); -} - -bool FullScreenIsActive(void) { - return GW->is_fullscreen(); -} - -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &val) { - Gdk::Rectangle rect = GW->get_widget().get_allocation(); - - // Convert to ij (vs. xy) style coordinates, - // and compensate for the input widget height due to inverse coord - int i, j; - i = x + rect.get_width() / 2 * GW->get_widget().get_scale_factor(); - j = -y + rect.get_height() / 2 * GW->get_widget().get_scale_factor(); - - GW->get_overlay().start_editing(i, j, fontHeight, /*is_monospace=*/false, minWidthChars, val); -} - -void HideGraphicsEditControl(void) { - GW->get_overlay().stop_editing(); -} - -bool GraphicsEditControlIsVisible(void) { - return GW->get_overlay().is_editing(); -} - /* Save/load */ static std::string ConvertFilters(std::string active, const FileFilter ssFilters[], @@ -630,7 +197,8 @@ static std::string ConvertFilters(std::string active, const FileFilter ssFilters bool GetOpenFile(Platform::Path *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { - Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Open File"))); + Gtk::FileChooserDialog chooser(*(Gtk::Window *)SS.GW.window->NativePtr(), + Title(C_("title", "Open File"))); chooser.set_filename(filename->raw); chooser.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); chooser.add_button(_("_Open"), Gtk::RESPONSE_OK); @@ -672,7 +240,8 @@ static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser) bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]) { - Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Save File")), + Gtk::FileChooserDialog chooser(*(Gtk::Window *)SS.GW.window->NativePtr(), + Title(C_("title", "Save File")), Gtk::FILE_CHOOSER_ACTION_SAVE); chooser.set_do_overwrite_confirmation(true); chooser.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); @@ -706,7 +275,8 @@ DialogChoice SaveFileYesNoCancel(void) { Glib::ustring message = _("The file has changed since it was last saved.\n\n" "Do you want to save the changes?"); - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title(Title(C_("title", "Modified File"))); dialog.add_button(C_("button", "_Save"), Gtk::RESPONSE_YES); @@ -730,7 +300,8 @@ DialogChoice LoadAutosaveYesNo(void) { Glib::ustring message = _("An autosave file is available for this project.\n\n" "Do you want to load the autosave file instead?"); - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title(Title(C_("title", "Autosave Available"))); dialog.add_button(C_("button", "_Load autosave"), Gtk::RESPONSE_YES); @@ -753,7 +324,8 @@ DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, "Do you want to locate it manually?\n\n" "If you select \"No\", any geometry that depends on " "the missing file will be removed."; - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title(Title(C_("title", "Missing File"))); dialog.add_button(C_("button", "_Yes"), Gtk::RESPONSE_YES); @@ -774,197 +346,11 @@ DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, } } -/* Text window */ - -class TextWidget : public Gtk::GLArea { -public: - TextWidget(Glib::RefPtr adjustment) : _adjustment(adjustment) { - set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | - Gdk::LEAVE_NOTIFY_MASK); - set_has_depth_buffer(true); - } - - void set_cursor_hand(bool is_hand) { - Glib::RefPtr gdkwin = get_window(); - if(gdkwin) { // returns NULL if not realized - Gdk::CursorType type = is_hand ? Gdk::HAND1 : Gdk::ARROW; - gdkwin->set_cursor(Gdk::Cursor::create(type)); - } - } - -protected: - // See GraphicsWidget::on_create_context. - Glib::RefPtr on_create_context() override { - return get_window()->create_gl_context(); - } - - bool on_render(const Glib::RefPtr &context) override { - SS.TW.Paint(); - return true; - } - - bool on_motion_notify_event(GdkEventMotion *event) override { - SS.TW.MouseEvent(/*leftClick*/ false, - /*leftDown*/ event->state & GDK_BUTTON1_MASK, - event->x * get_scale_factor(), - event->y * get_scale_factor()); - - return true; - } - - bool on_button_press_event(GdkEventButton *event) override { - SS.TW.MouseEvent(/*leftClick*/ event->type == GDK_BUTTON_PRESS, - /*leftDown*/ event->state & GDK_BUTTON1_MASK, - event->x * get_scale_factor(), - event->y * get_scale_factor()); - - return true; - } - - bool on_scroll_event(GdkEventScroll *event) override { - _adjustment->set_value(_adjustment->get_value() + - DeltaYOfScrollEvent(event) * _adjustment->get_page_increment()); - - return true; - } - - bool on_leave_notify_event (GdkEventCrossing *) override { - SS.TW.MouseLeave(); - - return true; - } - -private: - Glib::RefPtr _adjustment; -}; - -class TextWindowGtk : public Gtk::Window { -public: - TextWindowGtk() : _scrollbar(), _widget(_scrollbar.get_adjustment()), - _overlay(_widget), _box() { - set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY); - set_skip_taskbar_hint(true); - set_skip_pager_hint(true); - set_default_size(420, 300); - - _box.pack_start(_overlay, true, true); - _box.pack_start(_scrollbar, false, true); - add(_box); - - _scrollbar.get_adjustment()->signal_value_changed(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_scrollbar_value_changed)); - - _overlay.signal_editing_done(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_editing_done)); - - _overlay.get_entry().signal_motion_notify_event(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_motion_notify_event)); - _overlay.get_entry().signal_button_press_event(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_button_press_event)); - } - - Gtk::VScrollbar &get_scrollbar() { - return _scrollbar; - } - - TextWidget &get_widget() { - return _widget; - } - - EditorOverlay &get_overlay() { - return _overlay; - } - -protected: - void on_show() override { - Gtk::Window::on_show(); - - CnfThawWindowPos(this, "TextWindow"); - } - - void on_hide() override { - CnfFreezeWindowPos(this, "TextWindow"); - - Gtk::Window::on_hide(); - } - - bool on_delete_event(GdkEventAny *) override { - /* trigger the action and ignore the request */ - GraphicsWindow::MenuView(Command::SHOW_TEXT_WND); - - return false; - } - - void on_scrollbar_value_changed() { - SS.TW.ScrollbarEvent((int)_scrollbar.get_adjustment()->get_value()); - } - - void on_editing_done(Glib::ustring value) { - SS.TW.EditControlDone(value.c_str()); - } - - bool on_editor_motion_notify_event(GdkEventMotion *event) { - return _widget.event((GdkEvent*) event); - } - - bool on_editor_button_press_event(GdkEventButton *event) { - return _widget.event((GdkEvent*) event); - } - -private: - Gtk::VScrollbar _scrollbar; - TextWidget _widget; - EditorOverlay _overlay; - Gtk::HBox _box; -}; - -std::unique_ptr TW; - -void ShowTextWindow(bool visible) { - if(visible) - TW->show(); - else - TW->hide(); -} - -void GetTextWindowSize(int *w, int *h) { - Gdk::Rectangle allocation = TW->get_widget().get_allocation(); - *w = allocation.get_width() * TW->get_scale_factor(); - *h = allocation.get_height() * TW->get_scale_factor(); -} - -double GetScreenDpi() { - return Gdk::Screen::get_default()->get_resolution(); -} - -void InvalidateText(void) { - TW->get_widget().queue_draw(); -} - -void MoveTextScrollbarTo(int pos, int maxPos, int page) { - TW->get_scrollbar().get_adjustment()->configure(pos, 0, maxPos, 1, 10, page); -} - -void SetMousePointerToHand(bool is_hand) { - TW->get_widget().set_cursor_hand(is_hand); -} - -void ShowTextEditControl(int x, int y, const std::string &val) { - TW->get_overlay().start_editing(x, y, TextWindow::CHAR_HEIGHT, /*is_monospace=*/true, 30, val); -} - -void HideTextEditControl(void) { - TW->get_overlay().stop_editing(); -} - -bool TextEditControlIsVisible(void) { - return TW->get_overlay().is_editing(); -} - /* Miscellanea */ void DoMessageBox(const char *message, int rows, int cols, bool error) { - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, error ? Gtk::MESSAGE_ERROR : Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, /*is_modal*/ true); dialog.set_title(error ? @@ -1031,23 +417,6 @@ static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *, gpointer) } #endif -/* Application lifecycle */ - -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"))); -} - -void ExitNow() { - GW->hide(); - TW->hide(); -} }; int main(int argc, char** argv) { @@ -1091,21 +460,6 @@ int main(int argc, char** argv) { CnfLoad(); - auto icon = LoadPng("freedesktop/solvespace-48x48.png"); - auto icon_gdk = - Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB, - icon->format == SolveSpace::Pixmap::Format::RGBA, 8, - icon->width, icon->height, icon->stride); - - TW.reset(new TextWindowGtk); - GW.reset(new GraphicsWindowGtk); - TW->set_transient_for(*GW); - GW->set_icon(icon_gdk); - TW->set_icon(icon_gdk); - - TW->show_all(); - GW->show_all(); - const char* const* langNames = g_get_language_names(); while(*langNames) { if(SetLocale(*langNames++)) break; @@ -1114,16 +468,17 @@ int main(int argc, char** argv) { SetLocale("en_US"); } + SS.Init(); + #if defined(HAVE_SPACEWARE) && defined(GDK_WINDOWING_X11) if(GDK_IS_X11_DISPLAY(Gdk::Display::get_default()->gobj())) { // We don't care if it can't be opened; just continue without. spnav_x11_open(gdk_x11_get_default_xdisplay(), - gdk_x11_window_get_xid(GW->get_window()->gobj())); + gdk_x11_window_get_xid(((Gtk::Window *)SS.GW.window->NativePtr()) + ->get_window()->gobj())); } #endif - SS.Init(); - if(argc >= 2) { if(argc > 2) { dbp("Only the first file passed on command line will be opened."); @@ -1134,10 +489,7 @@ int main(int argc, char** argv) { SS.Load(Platform::Path::From(arg).Expand(/*fromCurrentDirectory=*/true)); } - main.run(*GW); - - TW.reset(); - GW.reset(); + main.run(); SK.Clear(); SS.Clear(); diff --git a/src/platform/gui.h b/src/platform/gui.h index 1faded98..81a6306e 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -13,6 +13,36 @@ namespace Platform { // Events //----------------------------------------------------------------------------- +// A mouse input event. +class MouseEvent { +public: + enum class Type { + MOTION, + PRESS, + DBL_PRESS, + RELEASE, + SCROLL_VERT, + LEAVE, + }; + + enum class Button { + NONE, + LEFT, + MIDDLE, + RIGHT, + }; + + Type type; + double x; + double y; + bool shiftDown; + bool controlDown; + union { + Button button; // for Type::{MOTION,PRESS,DBL_PRESS,RELEASE} + double scrollDelta; // for Type::SCROLL_VERT + }; +}; + // A keyboard input event. struct KeyboardEvent { enum class Type { @@ -106,7 +136,6 @@ public: virtual std::shared_ptr AddSubMenu(const std::string &label) = 0; virtual void Clear() = 0; - virtual void *NativePtr() = 0; }; typedef std::shared_ptr MenuBarRef; @@ -114,6 +143,87 @@ typedef std::shared_ptr MenuBarRef; MenuRef CreateMenu(); MenuBarRef GetOrCreateMainMenu(bool *unique); +// A native top-level window, with an OpenGL context, and an editor overlay. +class Window { +public: + enum class Kind { + TOPLEVEL, + TOOL, + }; + + enum class Cursor { + POINTER, + HAND + }; + + std::function onClose; + std::function onFullScreen; + std::function onMouseEvent; + std::function onKeyboardEvent; + std::function onEditingDone; + std::function onScrollbarAdjusted; + std::function onRender; + + virtual ~Window() {} + + // Returns physical display DPI. + virtual double GetPixelDensity() = 0; + // Returns raster graphics and coordinate scale (already applied on the platform side), + // i.e. size of logical pixel in physical pixels, or device pixel ratio. + virtual int GetDevicePixelRatio() = 0; + // Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio. + virtual double GetDeviceFontScale() { + return GetPixelDensity() / GetDevicePixelRatio() / 96.0; + } + + virtual bool IsVisible() = 0; + virtual void SetVisible(bool visible) = 0; + virtual void Focus() = 0; + + virtual bool IsFullScreen() = 0; + virtual void SetFullScreen(bool fullScreen) = 0; + + virtual void SetTitle(const std::string &title) = 0; + virtual bool SetTitleForFilename(const Path &filename) { return false; } + + virtual void SetMenuBar(MenuBarRef menuBar) = 0; + + virtual void GetContentSize(double *width, double *height) = 0; + virtual void SetMinContentSize(double width, double height) = 0; + + virtual void FreezePosition(const std::string &key) = 0; + virtual void ThawPosition(const std::string &key) = 0; + + virtual void SetCursor(Cursor cursor) = 0; + virtual void SetTooltip(const std::string &text) = 0; + + virtual bool IsEditorVisible() = 0; + virtual void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) = 0; + virtual void HideEditor() = 0; + + virtual void SetScrollbarVisible(bool visible) = 0; + virtual void ConfigureScrollbar(double min, double max, double pageSize) = 0; + virtual double GetScrollbarPosition() = 0; + virtual void SetScrollbarPosition(double pos) = 0; + + virtual void Invalidate() = 0; + virtual void Redraw() = 0; + + virtual void *NativePtr() = 0; +}; + +typedef std::shared_ptr WindowRef; + +WindowRef CreateWindow(Window::Kind kind = Window::Kind::TOPLEVEL, + WindowRef parentWindow = NULL); + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit(); + } #endif diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp index 8b13885a..cbd3bd12 100644 --- a/src/platform/guigtk.cpp +++ b/src/platform/guigtk.cpp @@ -3,12 +3,19 @@ // // Copyright 2018 whitequark //----------------------------------------------------------------------------- +#include "solvespace.h" #include +#include #include -#include +#include +#include +#include +#include #include #include -#include "solvespace.h" +#include +#include +#include namespace SolveSpace { namespace Platform { @@ -248,10 +255,6 @@ public: gtkMenuBar.foreach([&](Gtk::Widget &w) { gtkMenuBar.remove(w); }); subMenus.clear(); } - - void *NativePtr() override { - return >kMenuBar; - } }; MenuBarRef GetOrCreateMainMenu(bool *unique) { @@ -259,5 +262,584 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) { return std::make_shared(); } +//----------------------------------------------------------------------------- +// GTK GL and window extensions +//----------------------------------------------------------------------------- + +class GtkGLWidget : public Gtk::GLArea { + Window *_receiver; + +public: + GtkGLWidget(Platform::Window *receiver) : _receiver(receiver) { + set_has_depth_buffer(true); + set_can_focus(true); + set_events(Gdk::POINTER_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::BUTTON_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK); + } + +protected: + // Work around a bug fixed in GTKMM 3.22: + // https://mail.gnome.org/archives/gtkmm-list/2016-April/msg00020.html + Glib::RefPtr on_create_context() override { + return get_window()->create_gl_context(); + } + + bool on_render(const Glib::RefPtr &context) override { + if(_receiver->onRender) { + _receiver->onRender(); + } + return true; + } + + bool process_pointer_event(MouseEvent::Type type, double x, double y, + guint state, guint button = 0, int scroll_delta = 0) { + MouseEvent event = {}; + event.type = type; + event.x = x; + event.y = y; + if(button == 1 || (state & GDK_BUTTON1_MASK) != 0) { + event.button = MouseEvent::Button::LEFT; + } else if(button == 2 || (state & GDK_BUTTON2_MASK) != 0) { + event.button = MouseEvent::Button::MIDDLE; + } else if(button == 3 || (state & GDK_BUTTON3_MASK) != 0) { + event.button = MouseEvent::Button::RIGHT; + } + if((state & GDK_SHIFT_MASK) != 0) { + event.shiftDown = true; + } + if((state & GDK_CONTROL_MASK) != 0) { + event.controlDown = true; + } + if(scroll_delta != 0) { + event.scrollDelta = scroll_delta; + } + + if(_receiver->onMouseEvent) { + return _receiver->onMouseEvent(event); + } + + return false; + } + + bool on_motion_notify_event(GdkEventMotion *gdk_event) override { + if(process_pointer_event(MouseEvent::Type::MOTION, + gdk_event->x, gdk_event->y, gdk_event->state)) + return true; + + return Gtk::GLArea::on_motion_notify_event(gdk_event); + } + + bool on_button_press_event(GdkEventButton *gdk_event) override { + MouseEvent::Type type; + if(gdk_event->type == GDK_BUTTON_PRESS) { + type = MouseEvent::Type::PRESS; + } else if(gdk_event->type == GDK_2BUTTON_PRESS) { + type = MouseEvent::Type::DBL_PRESS; + } else { + return Gtk::GLArea::on_button_press_event(gdk_event); + } + + if(process_pointer_event(type, gdk_event->x, gdk_event->y, + gdk_event->state, gdk_event->button)) + return true; + + return Gtk::GLArea::on_button_press_event(gdk_event); + } + + bool on_button_release_event(GdkEventButton *gdk_event) override { + if(process_pointer_event(MouseEvent::Type::RELEASE, + gdk_event->x, gdk_event->y, + gdk_event->state, gdk_event->button)) + return true; + + return Gtk::GLArea::on_button_release_event(gdk_event); + } + + bool on_scroll_event(GdkEventScroll *gdk_event) override { + int delta; + if(gdk_event->delta_y < 0 || gdk_event->direction == GDK_SCROLL_UP) { + delta = 1; + } else if(gdk_event->delta_y > 0 || gdk_event->direction == GDK_SCROLL_DOWN) { + delta = -1; + } else { + return false; + } + + if(process_pointer_event(MouseEvent::Type::SCROLL_VERT, + gdk_event->x, gdk_event->y, + gdk_event->state, 0, delta)) + return true; + + return Gtk::GLArea::on_scroll_event(gdk_event); + } + + bool on_leave_notify_event(GdkEventCrossing *gdk_event) override { + if(process_pointer_event(MouseEvent::Type::LEAVE, + gdk_event->x, gdk_event->y, gdk_event->state)) + return true; + + return Gtk::GLArea::on_leave_notify_event(gdk_event); + } + + bool process_key_event(KeyboardEvent::Type type, GdkEventKey *gdk_event) { + KeyboardEvent event = {}; + event.type = type; + + if(gdk_event->state & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + return false; + } + + 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 = KeyboardEvent::Key::CHARACTER; + event.chr = chr; + } else if(gdk_event->keyval >= GDK_KEY_F1 && + gdk_event->keyval <= GDK_KEY_F12) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = gdk_event->keyval - GDK_KEY_F1 + 1; + } else { + return false; + } + + if(SS.GW.KeyboardEvent(event)) { + return true; + } + + return false; + } + + bool on_key_press_event(GdkEventKey *gdk_event) override { + if(process_key_event(KeyboardEvent::Type::PRESS, gdk_event)) + return true; + + return Gtk::GLArea::on_key_press_event(gdk_event); + } + + bool on_key_release_event(GdkEventKey *gdk_event) override { + if(process_key_event(KeyboardEvent::Type::RELEASE, gdk_event)) + return true; + + return Gtk::GLArea::on_key_release_event(gdk_event); + } +}; + +class GtkEditorOverlay : public Gtk::Fixed { + Window *_receiver; + GtkGLWidget _gl_widget; + Gtk::Entry _entry; + +public: + GtkEditorOverlay(Platform::Window *receiver) : _receiver(receiver), _gl_widget(receiver) { + add(_gl_widget); + + _entry.set_no_show_all(true); + _entry.set_has_frame(false); + add(_entry); + + _entry.signal_activate(). + connect(sigc::mem_fun(this, &GtkEditorOverlay::on_activate)); + } + + bool is_editing() const { + return _entry.is_visible(); + } + + void start_editing(int x, int y, int font_height, int min_width, bool is_monospace, + const std::string &val) { + Pango::FontDescription font_desc; + font_desc.set_family(is_monospace ? "monospace" : "normal"); + font_desc.set_absolute_size(font_height * Pango::SCALE); + _entry.override_font(font_desc); + + // The y coordinate denotes baseline. + Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc); + y -= font_metrics.get_ascent() / Pango::SCALE; + + Glib::RefPtr layout = Pango::Layout::create(get_pango_context()); + layout->set_font_description(font_desc); + // Add one extra char width to avoid scrolling. + layout->set_text(val + " "); + int width = layout->get_logical_extents().get_width(); + + Gtk::Border margin = _entry.get_style_context()->get_margin(); + Gtk::Border border = _entry.get_style_context()->get_border(); + Gtk::Border padding = _entry.get_style_context()->get_padding(); + move(_entry, + x - margin.get_left() - border.get_left() - padding.get_left(), + y - margin.get_top() - border.get_top() - padding.get_top()); + + int fitWidth = width / Pango::SCALE + padding.get_left() + padding.get_right(); + _entry.set_size_request(max(fitWidth, min_width), -1); + queue_resize(); + + _entry.set_text(val); + + if(!_entry.is_visible()) { + _entry.show(); + _entry.grab_focus(); + + // We grab the input for ourselves and not the entry to still have + // the pointer events go through the underlay. + add_modal_grab(); + } + } + + void stop_editing() { + if(_entry.is_visible()) { + remove_modal_grab(); + _entry.hide(); + _gl_widget.grab_focus(); + } + } + + GtkGLWidget &get_gl_widget() { + return _gl_widget; + } + +protected: + bool on_key_press_event(GdkEventKey *gdk_event) override { + if(is_editing()) { + if(gdk_event->keyval == GDK_KEY_Escape) { + stop_editing(); + } else { + _entry.event((GdkEvent *)gdk_event); + } + return true; + } + + return Gtk::Fixed::on_key_press_event(gdk_event); + } + + bool on_key_release_event(GdkEventKey *gdk_event) override { + if(is_editing()) { + _entry.event((GdkEvent *)gdk_event); + return true; + } + + return Gtk::Fixed::on_key_release_event(gdk_event); + } + + void on_size_allocate(Gtk::Allocation& allocation) override { + Gtk::Fixed::on_size_allocate(allocation); + + _gl_widget.size_allocate(allocation); + + int width, height, min_height, natural_height; + _entry.get_size_request(width, height); + _entry.get_preferred_height(min_height, natural_height); + + Gtk::Allocation entry_rect = _entry.get_allocation(); + entry_rect.set_width(width); + entry_rect.set_height(natural_height); + _entry.size_allocate(entry_rect); + } + + void on_activate() { + if(_receiver->onEditingDone) { + _receiver->onEditingDone(_entry.get_text()); + } + } +}; + +class GtkWindow : public Gtk::Window { + Platform::Window *_receiver; + Gtk::VBox _vbox; + Gtk::MenuBar *_menu_bar; + Gtk::HBox _hbox; + GtkEditorOverlay _editor_overlay; + Gtk::VScrollbar _scrollbar; + bool _is_fullscreen; + +public: + GtkWindow(Platform::Window *receiver) : + _receiver(receiver), _menu_bar(NULL), _editor_overlay(receiver) { + _hbox.pack_start(_editor_overlay, /*expand=*/true, /*fill=*/true); + _hbox.pack_end(_scrollbar, /*expand=*/false, /*fill=*/false); + _vbox.pack_end(_hbox, /*expand=*/true, /*fill=*/true); + add(_vbox); + + _vbox.show(); + _hbox.show(); + _editor_overlay.show(); + get_gl_widget().show(); + + _scrollbar.get_adjustment()->signal_value_changed(). + connect(sigc::mem_fun(this, &GtkWindow::on_scrollbar_value_changed)); + } + + bool is_full_screen() const { + return _is_fullscreen; + } + + Gtk::MenuBar *get_menu_bar() const { + return _menu_bar; + } + + void set_menu_bar(Gtk::MenuBar *menu_bar) { + if(_menu_bar) { + _vbox.remove(*_menu_bar); + } + _menu_bar = menu_bar; + if(_menu_bar) { + _menu_bar->show_all(); + _vbox.pack_start(*_menu_bar, /*expand=*/false, /*fill=*/false); + } + } + + GtkEditorOverlay &get_editor_overlay() { + return _editor_overlay; + } + + GtkGLWidget &get_gl_widget() { + return _editor_overlay.get_gl_widget(); + } + + Gtk::VScrollbar &get_scrollbar() { + return _scrollbar; + } + +protected: + bool on_delete_event(GdkEventAny* gdk_event) { + if(_receiver->onClose) { + _receiver->onClose(); + return true; + } + + return false; + } + + bool on_window_state_event(GdkEventWindowState *gdk_event) override { + _is_fullscreen = gdk_event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; + if(_receiver->onFullScreen) { + _receiver->onFullScreen(_is_fullscreen); + } + + return Gtk::Window::on_window_state_event(gdk_event); + } + + void on_scrollbar_value_changed() { + if(_receiver->onScrollbarAdjusted) { + _receiver->onScrollbarAdjusted(_scrollbar.get_adjustment()->get_value()); + } + } +}; + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class WindowImplGtk : public Window { +public: + GtkWindow gtkWindow; + MenuBarRef menuBar; + + WindowImplGtk(Window::Kind kind) : gtkWindow(this) { + switch(kind) { + case Kind::TOPLEVEL: + break; + + case Kind::TOOL: + gtkWindow.set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY); + gtkWindow.set_skip_taskbar_hint(true); + gtkWindow.set_skip_pager_hint(true); + break; + } + + auto icon = LoadPng("freedesktop/solvespace-48x48.png"); + auto gdkIcon = + Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB, + icon->format == Pixmap::Format::RGBA, 8, + icon->width, icon->height, icon->stride); + gtkWindow.set_icon(gdkIcon->copy()); + } + + double GetPixelDensity() override { + return gtkWindow.get_screen()->get_resolution(); + } + + int GetDevicePixelRatio() override { + return gtkWindow.get_scale_factor(); + } + + bool IsVisible() override { + return gtkWindow.is_visible(); + } + + void SetVisible(bool visible) override { + if(visible) { + gtkWindow.show(); + } else { + gtkWindow.hide(); + } + } + + void Focus() override { + gtkWindow.present(); + } + + bool IsFullScreen() override { + return gtkWindow.is_full_screen(); + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen) { + gtkWindow.fullscreen(); + } else { + gtkWindow.unfullscreen(); + } + } + + void SetTitle(const std::string &title) override { + gtkWindow.set_title(title + " — SolveSpace"); + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + if(newMenuBar) { + Gtk::MenuBar *gtkMenuBar = &((MenuBarImplGtk*)&*newMenuBar)->gtkMenuBar; + gtkWindow.set_menu_bar(gtkMenuBar); + } else { + gtkWindow.set_menu_bar(NULL); + } + menuBar = newMenuBar; + } + + void GetContentSize(double *width, double *height) override { + *width = gtkWindow.get_gl_widget().get_allocated_width(); + *height = gtkWindow.get_gl_widget().get_allocated_height(); + } + + void SetMinContentSize(double width, double height) override { + gtkWindow.get_gl_widget().set_size_request(width, height); + } + + void FreezePosition(const std::string &key) override { + if(!gtkWindow.is_visible()) return; + + int left, top, width, height; + gtkWindow.get_position(left, top); + gtkWindow.get_size(width, height); + bool isMaximized = gtkWindow.is_maximized(); + + CnfFreezeInt(left, key + "_left"); + CnfFreezeInt(top, key + "_top"); + CnfFreezeInt(width, key + "_width"); + CnfFreezeInt(height, key + "_height"); + CnfFreezeInt(isMaximized, key + "_maximized"); + } + + void ThawPosition(const std::string &key) override { + int left, top, width, height; + gtkWindow.get_position(left, top); + gtkWindow.get_size(width, height); + + left = CnfThawInt(left, key + "_left"); + top = CnfThawInt(top, key + "_top"); + width = CnfThawInt(width, key + "_width"); + height = CnfThawInt(height, key + "_height"); + + gtkWindow.move(left, top); + gtkWindow.resize(width, height); + + if(CnfThawInt(false, key + "_maximized")) { + gtkWindow.maximize(); + } + } + + void SetCursor(Cursor cursor) override { + Gdk::CursorType gdkCursorType; + switch(cursor) { + case Cursor::POINTER: gdkCursorType = Gdk::ARROW; break; + case Cursor::HAND: gdkCursorType = Gdk::HAND1; break; + } + + auto gdkWindow = gtkWindow.get_gl_widget().get_window(); + if(gdkWindow) { + gdkWindow->set_cursor(Gdk::Cursor::create(gdkCursorType)); + } + } + + void SetTooltip(const std::string &text) override { + if(text.empty()) { + gtkWindow.get_gl_widget().set_has_tooltip(false); + } else { + gtkWindow.get_gl_widget().set_tooltip_text(text); + } + } + + bool IsEditorVisible() override { + return gtkWindow.get_editor_overlay().is_editing(); + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + gtkWindow.get_editor_overlay().start_editing( + x, y, fontHeight, minWidth, isMonospace, text); + } + + void HideEditor() override { + gtkWindow.get_editor_overlay().stop_editing(); + } + + void SetScrollbarVisible(bool visible) override { + if(visible) { + gtkWindow.get_scrollbar().show(); + } else { + gtkWindow.get_scrollbar().hide(); + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + auto adjustment = gtkWindow.get_scrollbar().get_adjustment(); + adjustment->configure(adjustment->get_value(), min, max, 1, 4, pageSize); + } + + double GetScrollbarPosition() override { + return gtkWindow.get_scrollbar().get_adjustment()->get_value(); + } + + void SetScrollbarPosition(double pos) override { + return gtkWindow.get_scrollbar().get_adjustment()->set_value(pos); + } + + void Invalidate() override { + gtkWindow.get_gl_widget().queue_render(); + } + + void Redraw() override { + Invalidate(); + Gtk::Main::iteration(/*blocking=*/false); + } + + void *NativePtr() override { + return >kWindow; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + auto window = std::make_shared(kind); + if(parentWindow) { + window->gtkWindow.set_transient_for( + std::static_pointer_cast(parentWindow)->gtkWindow); + } + return window; +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + Gtk::Main::quit(); +} + } } diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm index 3c346b17..7b04d68b 100644 --- a/src/platform/guimac.mm +++ b/src/platform/guimac.mm @@ -6,6 +6,19 @@ #import #include "solvespace.h" +using namespace SolveSpace; + +//----------------------------------------------------------------------------- +// Internal AppKit classes +//----------------------------------------------------------------------------- + +@interface NSToolTipManager : NSObject ++ (NSToolTipManager *)sharedToolTipManager; +- (void)setInitialToolTipDelay:(double)delay; +- (void)orderOutToolTip; +- (void)_displayTemporaryToolTipForView:(id)arg1 withString:(id)arg2; +@end + //----------------------------------------------------------------------------- // Objective-C bridging //----------------------------------------------------------------------------- @@ -215,10 +228,6 @@ public: } subMenus.clear(); } - - void *NativePtr() override { - return NULL; - } }; MenuBarRef GetOrCreateMainMenu(bool *unique) { @@ -230,7 +239,677 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) { return mainMenu; } -void SetMainMenu(MenuBarRef menuBar) { +} +} + +//----------------------------------------------------------------------------- +// Cocoa NSView and NSWindow extensions +//----------------------------------------------------------------------------- + +@interface SSView : NSView +@property Platform::Window *receiver; + +@property BOOL acceptsFirstResponder; + +@property(readonly, getter=isEditing) BOOL editing; +- (void)startEditing:(NSString *)text at:(NSPoint)origin + withHeight:(double)fontHeight minWidth:(double)minWidth + usingMonospace:(BOOL)isMonospace; +- (void)stopEditing; +- (void)didEdit:(NSString *)text; + +@property double scrollerMin; +@property double scrollerMax; +@end + +@implementation SSView +{ + GlOffscreen offscreen; + NSOpenGLContext *glContext; + NSTrackingArea *trackingArea; + NSTextField *editor; +} + +- (id)initWithFrame:(NSRect)frameRect { + if(self = [super initWithFrame:frameRect]) { + self.wantsLayer = YES; + + NSOpenGLPixelFormatAttribute attrs[] = { + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 24, + 0 + }; + NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + glContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:NULL]; + + editor = [[NSTextField alloc] init]; + editor.editable = YES; + [[editor cell] setWraps:NO]; + [[editor cell] setScrollable:YES]; + editor.bezeled = NO; + editor.target = self; + editor.action = @selector(didEdit:); + } + return self; +} + +- (void)dealloc { + offscreen.Clear(); +} + +- (BOOL)isFlipped { + return YES; +} + +@synthesize receiver; + +- (void)drawRect:(NSRect)aRect { + [glContext makeCurrentContext]; + + NSSize size = [self convertSizeToBacking:self.bounds.size]; + int width = (int)size.width, + height = (int)size.height; + offscreen.Render(width, height, [&] { + if(receiver->onRender) { + receiver->onRender(); + } + }); + + CGDataProviderRef provider = CGDataProviderCreateWithData( + NULL, &offscreen.data[0], width * height * 4, NULL); + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGImageRef image = CGImageCreate(width, height, 8, 32, + width * 4, colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, + provider, NULL, true, kCGRenderingIntentDefault); + + CGContextDrawImage((CGContextRef) [[NSGraphicsContext currentContext] graphicsPort], + [self bounds], image); + + CGImageRelease(image); + CGDataProviderRelease(provider); +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event { + return YES; +} + +- (void)updateTrackingAreas { + [self removeTrackingArea:trackingArea]; + trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + ([self acceptsFirstResponder] + ? NSTrackingActiveInKeyWindow + : NSTrackingActiveAlways)) + owner:self userInfo:nil]; + [self addTrackingArea:trackingArea]; + [super updateTrackingAreas]; +} + +- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent { + Platform::MouseEvent event = {}; + + NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:self]; + event.x = nsPoint.x; + event.y = self.bounds.size.height - nsPoint.y; + + NSUInteger nsFlags = [nsEvent modifierFlags]; + if(nsFlags & NSShiftKeyMask) event.shiftDown = true; + if(nsFlags & NSCommandKeyMask) event.controlDown = true; + + return event; +} + +- (void)mouseMotionEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::MOTION; + event.button = MouseEvent::Button::NONE; + + NSUInteger nsButtons = [NSEvent pressedMouseButtons]; + if(nsButtons & (1 << 0)) { + event.button = MouseEvent::Button::LEFT; + } else if(nsButtons & (1 << 1)) { + event.button = MouseEvent::Button::RIGHT; + } else if(nsButtons & (1 << 2)) { + event.button = MouseEvent::Button::MIDDLE; + } + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseMoved:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; + [super mouseMoved:nsEvent]; +} + +- (void)mouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)otherMouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)rightMouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)mouseButtonEvent:(NSEvent *)nsEvent withType:(Platform::MouseEvent::Type)type { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = type; + if([nsEvent buttonNumber] == 0) { + event.button = MouseEvent::Button::LEFT; + } else if([nsEvent buttonNumber] == 1) { + event.button = MouseEvent::Button::RIGHT; + } else if([nsEvent buttonNumber] == 2) { + event.button = MouseEvent::Button::MIDDLE; + } else return; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseDownEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent::Type type; + if([nsEvent clickCount] == 1) { + type = MouseEvent::Type::PRESS; + } else { + type = MouseEvent::Type::DBL_PRESS; + } + [self mouseButtonEvent:nsEvent withType:type]; +} + +- (void)mouseUpEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + [self mouseButtonEvent:nsEvent withType:(MouseEvent::Type::RELEASE)]; +} + +- (void)mouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; +} + +- (void)otherMouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; +} + +- (void)rightMouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; + [super rightMouseDown:nsEvent]; +} + +- (void)mouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)otherMouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)rightMouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)scrollWheel:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = [nsEvent deltaY]; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseExited:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::LEAVE; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (Platform::KeyboardEvent)convertKeyboardEvent:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + KeyboardEvent event = {}; + + NSUInteger nsFlags = [nsEvent modifierFlags]; + if(nsFlags & NSShiftKeyMask) + event.shiftDown = true; + if(nsFlags & NSCommandKeyMask) + event.controlDown = true; + + unichar chr = 0; + if(NSString *nsChr = [[nsEvent charactersIgnoringModifiers] lowercaseString]) { + chr = [nsChr characterAtIndex:0]; + } + if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = chr - NSF1FunctionKey + 1; + } else { + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = chr; + } + + return event; +} + +- (void)keyDown:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + if([NSEvent modifierFlags] & ~(NSShiftKeyMask|NSCommandKeyMask)) { + [super keyDown:nsEvent]; + return; + } + + KeyboardEvent event = [self convertKeyboardEvent:nsEvent]; + event.type = KeyboardEvent::Type::PRESS; + + if(receiver->onKeyboardEvent) { + receiver->onKeyboardEvent(event); + return; + } + + [super keyDown:nsEvent]; +} + +- (void)keyUp:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + if([NSEvent modifierFlags] & ~(NSShiftKeyMask|NSCommandKeyMask)) { + [super keyUp:nsEvent]; + return; + } + + KeyboardEvent event = [self convertKeyboardEvent:nsEvent]; + event.type = KeyboardEvent::Type::RELEASE; + + if(receiver->onKeyboardEvent) { + receiver->onKeyboardEvent(event); + return; + } + + [super keyUp:nsEvent]; +} + +@synthesize editing; + +- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight + minWidth:(double)minWidth usingMonospace:(BOOL)isMonospace { + if(!editing) { + [self addSubview:editor]; + editing = YES; + } + + if(isMonospace) { + editor.font = [NSFont fontWithName:@"Monaco" size:fontHeight]; + } else { + editor.font = [NSFont controlContentFontOfSize:fontHeight]; + } + + origin.x -= 3; /* left padding; no way to get it from NSTextField */ + origin.y -= [editor intrinsicContentSize].height; + origin.y += [editor baselineOffsetFromBottom]; + + [editor setFrameOrigin:origin]; + [editor setStringValue:text]; + [editor sizeToFit]; + + NSSize frameSize = [editor frame].size; + frameSize.width = std::max(frameSize.width, minWidth); + [editor setFrameSize:frameSize]; + + [[self window] makeFirstResponder:editor]; + [[self window] makeKeyWindow]; +} + +- (void)stopEditing { + if(editing) { + [editor removeFromSuperview]; + [[self window] makeFirstResponder:self]; + editing = NO; + } +} + +- (void)didEdit:(id)sender { + if(receiver->onEditingDone) { + receiver->onEditingDone([[editor stringValue] UTF8String]); + } +} + +- (void)cancelOperation:(id)sender { + using Platform::KeyboardEvent; + + if(receiver->onKeyboardEvent) { + KeyboardEvent event = {}; + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = '\e'; + event.type = KeyboardEvent::Type::PRESS; + receiver->onKeyboardEvent(event); + event.type = KeyboardEvent::Type::RELEASE; + receiver->onKeyboardEvent(event); + } +} + +@synthesize scrollerMin; +@synthesize scrollerMax; + +- (void)didScroll:(NSScroller *)sender { + if(receiver->onScrollbarAdjusted) { + double pos = scrollerMin + [sender doubleValue] * (scrollerMax - scrollerMin); + receiver->onScrollbarAdjusted(pos); + } +} +@end + +@interface SSWindowDelegate : NSObject +@property Platform::Window *receiver; + +- (BOOL)windowShouldClose:(id)sender; + +@property(readonly, getter=isFullScreen) BOOL fullScreen; +- (void)windowDidEnterFullScreen:(NSNotification *)notification; +- (void)windowDidExitFullScreen:(NSNotification *)notification; +@end + +@implementation SSWindowDelegate +@synthesize receiver; + +- (BOOL)windowShouldClose:(id)sender { + if(receiver->onClose) { + receiver->onClose(); + } + return NO; +} + +@synthesize fullScreen; + +- (void)windowDidEnterFullScreen:(NSNotification *)notification { + fullScreen = true; + if(receiver->onFullScreen) { + receiver->onFullScreen(fullScreen); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification { + fullScreen = false; + if(receiver->onFullScreen) { + receiver->onFullScreen(fullScreen); + } +} +@end + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class WindowImplCocoa : public Window { +public: + NSWindow *nsWindow; + SSWindowDelegate *ssWindowDelegate; + SSView *ssView; + NSScroller *nsScroller; + NSView *nsContainer; + + NSArray *nsConstraintsWithScrollbar; + NSArray *nsConstraintsWithoutScrollbar; + + double minWidth = 100.0; + double minHeight = 100.0; + + NSString *nsToolTip; + + WindowImplCocoa(Window::Kind kind, std::shared_ptr parentWindow) { + ssView = [[SSView alloc] init]; + ssView.translatesAutoresizingMaskIntoConstraints = NO; + ssView.receiver = this; + + nsScroller = [[NSScroller alloc] initWithFrame:NSMakeRect(0, 0, 0, 100)]; + nsScroller.translatesAutoresizingMaskIntoConstraints = NO; + nsScroller.enabled = YES; + nsScroller.scrollerStyle = NSScrollerStyleOverlay; + nsScroller.knobStyle = NSScrollerKnobStyleLight; + nsScroller.action = @selector(didScroll:); + nsScroller.target = ssView; + nsScroller.continuous = YES; + + nsContainer = [[NSView alloc] init]; + [nsContainer addSubview:ssView]; + [nsContainer addSubview:nsScroller]; + + NSDictionary *views = NSDictionaryOfVariableBindings(ssView, nsScroller); + nsConstraintsWithoutScrollbar = [NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[ssView]|" + options:0 metrics:nil views:views]; + [nsContainer addConstraints:nsConstraintsWithoutScrollbar]; + nsConstraintsWithScrollbar = [NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[ssView]-0-[nsScroller(11)]|" + options:0 metrics:nil views:views]; + [nsContainer addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"V:|[ssView]|" + options:0 metrics:nil views:views]]; + [nsContainer addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"V:|[nsScroller]|" + options:0 metrics:nil views:views]]; + + switch(kind) { + case Window::Kind::TOPLEVEL: + nsWindow = [[NSWindow alloc] init]; + nsWindow.styleMask = NSTitledWindowMask | NSResizableWindowMask | + NSClosableWindowMask | NSMiniaturizableWindowMask; + nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary; + ssView.acceptsFirstResponder = YES; + break; + + case Window::Kind::TOOL: + NSPanel *nsPanel = [[NSPanel alloc] init]; + nsPanel.styleMask = NSTitledWindowMask | NSResizableWindowMask | + NSClosableWindowMask | NSUtilityWindowMask; + [nsPanel standardWindowButton:NSWindowMiniaturizeButton].hidden = YES; + [nsPanel standardWindowButton:NSWindowZoomButton].hidden = YES; + nsPanel.floatingPanel = YES; + nsPanel.becomesKeyOnlyIfNeeded = YES; + nsWindow = nsPanel; + break; + } + + ssWindowDelegate = [[SSWindowDelegate alloc] init]; + ssWindowDelegate.receiver = this; + nsWindow.delegate = ssWindowDelegate; + + nsWindow.backgroundColor = [NSColor blackColor]; + nsWindow.contentView = nsContainer; + } + + double GetPixelDensity() override { + NSDictionary *description = nsWindow.screen.deviceDescription; + NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; + CGSize displayPhysicalSize = CGDisplayScreenSize( + [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f; + } + + int GetDevicePixelRatio() override { + NSSize unitSize = { 1.0f, 0.0f }; + unitSize = [ssView convertSizeToBacking:unitSize]; + return (int)unitSize.width; + } + + bool IsVisible() override { + return ![nsWindow isVisible]; + } + + void SetVisible(bool visible) override { + if(visible) { + [nsWindow orderFront:nil]; + } else { + [nsWindow close]; + } + } + + void Focus() override { + [nsWindow makeKeyAndOrderFront:nil]; + } + + bool IsFullScreen() override { + return ssWindowDelegate.fullScreen; + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen != IsFullScreen()) { + [nsWindow toggleFullScreen:nil]; + } + } + + void SetTitle(const std::string &title) override { + nsWindow.representedFilename = @""; + nsWindow.title = Wrap(title); + } + + bool SetTitleForFilename(const Path &filename) override { + [nsWindow setTitleWithRepresentedFilename:Wrap(filename.raw)]; + return true; + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + // Doesn't do anything, since we have an unique global menu bar. + } + + void GetContentSize(double *width, double *height) override { + NSSize nsSize = [ssView frame].size; + *width = nsSize.width; + *height = nsSize.height; + } + + void SetMinContentSize(double width, double height) { + NSSize nsMinSize; + nsMinSize.width = width; + nsMinSize.height = height; + [nsWindow setContentMinSize:nsMinSize]; + [nsWindow setContentSize:nsMinSize]; + } + + void FreezePosition(const std::string &key) override { + [nsWindow saveFrameUsingName:Wrap(key)]; + } + + void ThawPosition(const std::string &key) override { + [nsWindow setFrameUsingName:Wrap(key)]; + } + + void SetCursor(Cursor cursor) override { + NSCursor *nsNewCursor; + switch(cursor) { + case Cursor::POINTER: nsNewCursor = [NSCursor arrowCursor]; break; + case Cursor::HAND: nsNewCursor = [NSCursor pointingHandCursor]; break; + } + + if([NSCursor currentCursor] != nsNewCursor) { + [nsNewCursor set]; + } + } + + void SetTooltip(const std::string &newText) override { + NSString *nsNewText = Wrap(newText); + if(![[ssView toolTip] isEqualToString:nsNewText]) { + [ssView setToolTip:nsNewText]; + + NSToolTipManager *nsToolTipManager = [NSToolTipManager sharedToolTipManager]; + if(newText.empty()) { + [nsToolTipManager orderOutToolTip]; + } else { + [nsToolTipManager _displayTemporaryToolTipForView:ssView withString:Wrap(newText)]; + } + } + } + + bool IsEditorVisible() override { + return [ssView isEditing]; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + [ssView startEditing:Wrap(text) at:(NSPoint){(CGFloat)x, (CGFloat)y} + withHeight:fontHeight minWidth:minWidth usingMonospace:isMonospace]; + } + + void HideEditor() override { + [ssView stopEditing]; + } + + void SetScrollbarVisible(bool visible) override { + if(visible) { + [nsContainer removeConstraints:nsConstraintsWithoutScrollbar]; + [nsContainer addConstraints:nsConstraintsWithScrollbar]; + } else { + [nsContainer removeConstraints:nsConstraintsWithScrollbar]; + [nsContainer addConstraints:nsConstraintsWithoutScrollbar]; + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + ssView.scrollerMin = min; + ssView.scrollerMax = max - pageSize; + [nsScroller setKnobProportion:(pageSize / (ssView.scrollerMax - ssView.scrollerMin))]; + } + + double GetScrollbarPosition() override { + return ssView.scrollerMin + + [nsScroller doubleValue] * (ssView.scrollerMax - ssView.scrollerMin); + } + + void SetScrollbarPosition(double pos) override { + if(pos > ssView.scrollerMax) { + pos = ssView.scrollerMax; + } + [nsScroller setDoubleValue:(pos / (ssView.scrollerMax - ssView.scrollerMin))]; + if(onScrollbarAdjusted) { + onScrollbarAdjusted(pos); + } + } + + void Invalidate() override { + ssView.needsDisplay = YES; + } + + void Redraw() override { + Invalidate(); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); + } + + void *NativePtr() override { + return (__bridge void *)ssView; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + [NSApp setDelegate:nil]; + [NSApp terminate:nil]; } } diff --git a/src/platform/guinone.cpp b/src/platform/guinone.cpp index 416c86bf..71385f4e 100644 --- a/src/platform/guinone.cpp +++ b/src/platform/guinone.cpp @@ -8,6 +8,14 @@ namespace SolveSpace { +//----------------------------------------------------------------------------- +// Rendering +//----------------------------------------------------------------------------- + +std::shared_ptr CreateRenderer() { + return std::make_shared(); +} + namespace Platform { //----------------------------------------------------------------------------- @@ -27,50 +35,31 @@ TimerRef CreateTimer() { // 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(); + return std::shared_ptr(); } -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(); + return std::shared_ptr(); } +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + exit(0); } -void SetMainMenu(Platform::MenuBarRef menuBar) { } //----------------------------------------------------------------------------- @@ -143,106 +132,6 @@ std::string CnfThawString(const std::string &val, const std::string &key) { return ret; } -//----------------------------------------------------------------------------- -// Rendering -//----------------------------------------------------------------------------- - -std::shared_ptr CreateRenderer() { - return NULL; -} - -//----------------------------------------------------------------------------- -// Graphics window -//----------------------------------------------------------------------------- - -void GetGraphicsWindowSize(int *w, int *h) { - *w = *h = 600; -} -double GetScreenDpi() { - return 72; -} - -void InvalidateGraphics() { -} - -std::shared_ptr framebuffer; -bool antialias = true; -void PaintGraphics() { - const Camera &camera = SS.GW.GetCamera(); - - std::shared_ptr pixmap = std::make_shared(); - pixmap->format = Pixmap::Format::BGRA; - pixmap->width = camera.width; - pixmap->height = camera.height; - pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, (int)camera.width); - pixmap->data = std::vector(pixmap->stride * pixmap->height); - cairo_surface_t *surface = - cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, - (int)pixmap->width, (int)pixmap->height, - (int)pixmap->stride); - cairo_t *context = cairo_create(surface); - - CairoRenderer canvas; - canvas.camera = camera; - canvas.lighting = SS.GW.GetLighting(); - canvas.chordTolerance = SS.chordTol; - canvas.context = context; - canvas.antialias = antialias; - - SS.GW.Draw(&canvas); - canvas.CullOccludedStrokes(); - canvas.OutputInPaintOrder(); - - pixmap->ConvertTo(Pixmap::Format::RGBA); - framebuffer = pixmap; - - canvas.Clear(); - - cairo_surface_destroy(surface); - cairo_destroy(context); -} - -void SetCurrentFilename(const Platform::Path &filename) { -} -void ToggleFullScreen() { -} -bool FullScreenIsActive() { - return false; -} -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &val) { - ssassert(false, "Not implemented"); -} -void HideGraphicsEditControl() { -} -bool GraphicsEditControlIsVisible() { - return false; -} - -//----------------------------------------------------------------------------- -// Text window -//----------------------------------------------------------------------------- - -void ShowTextWindow(bool visible) { -} -void GetTextWindowSize(int *w, int *h) { - *w = *h = 100; -} -void InvalidateText() { -} -void MoveTextScrollbarTo(int pos, int maxPos, int page) { -} -void SetMousePointerToHand(bool is_hand) { -} -void ShowTextEditControl(int x, int y, const std::string &val) { - ssassert(false, "Not implemented"); -} -void HideTextEditControl() { -} -bool TextEditControlIsVisible() { - return false; -} - //----------------------------------------------------------------------------- // Dialogs //----------------------------------------------------------------------------- @@ -282,15 +171,4 @@ std::vector GetFontFiles() { return fontFiles; } -//----------------------------------------------------------------------------- -// Application lifecycle -//----------------------------------------------------------------------------- - -void RefreshLocale() { -} - -void ExitNow() { - ssassert(false, "Not implemented"); -} - } diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp index 85afafaa..19974505 100644 --- a/src/platform/guiwin.cpp +++ b/src/platform/guiwin.cpp @@ -3,9 +3,24 @@ // // Copyright 2018 whitequark //----------------------------------------------------------------------------- +#include "config.h" #include "solvespace.h" // Include after solvespace.h to avoid identifier clashes. #include +#include +#include + +#ifndef WM_DPICHANGED +#define WM_DPICHANGED 0x02E0 +#endif + +// We have our own CreateWindow. +#undef CreateWindow + +#if HAVE_OPENGL == 3 +#define EGLAPI /*static linkage*/ +#include +#endif namespace SolveSpace { namespace Platform { @@ -35,6 +50,58 @@ void CheckLastError(const char *file, int line, const char *function, const char } } +typedef UINT (WINAPI *LPFNGETDPIFORWINDOW)(HWND); + +UINT ssGetDpiForWindow(HWND hwnd) { + static bool checked; + static LPFNGETDPIFORWINDOW lpfnGetDpiForWindow; + if(!checked) { + checked = true; + lpfnGetDpiForWindow = (LPFNGETDPIFORWINDOW) + GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); + } + if(lpfnGetDpiForWindow) { + return lpfnGetDpiForWindow(hwnd); + } else { + HDC hDc; + sscheck(hDc = GetDC(HWND_DESKTOP)); + UINT dpi; + sscheck(dpi = GetDeviceCaps(hDc, LOGPIXELSX)); + sscheck(ReleaseDC(HWND_DESKTOP, hDc)); + return dpi; + } +} + +typedef BOOL (WINAPI *LPFNADJUSTWINDOWRECTEXFORDPI)(LPRECT, DWORD, BOOL, DWORD, UINT); + +BOOL ssAdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, + DWORD dwExStyle, UINT dpi) { + static bool checked; + static LPFNADJUSTWINDOWRECTEXFORDPI lpfnAdjustWindowRectExForDpi; + if(!checked) { + checked = true; + lpfnAdjustWindowRectExForDpi = (LPFNADJUSTWINDOWRECTEXFORDPI) + GetProcAddress(GetModuleHandleW(L"user32.dll"), "AdjustWindowRectExForDpi"); + } + if(lpfnAdjustWindowRectExForDpi) { + return lpfnAdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi); + } else { + return AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle); + } +} + +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +std::wstring Title(const std::string &s) { + return Widen("SolveSpace - " + s); +} + +static int Clamp(int x, int a, int b) { + return max(a, min(x, b)); +} + //----------------------------------------------------------------------------- // Timers //----------------------------------------------------------------------------- @@ -153,14 +220,7 @@ public: } }; -void TriggerMenu(int id) { - MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)id; - if(menuItem->onTrigger) { - menuItem->onTrigger(); - } -} - -int64_t contextMenuCancelTime = 0; +int64_t contextMenuPopTime = 0; class MenuImplWin32 : public Menu { public: @@ -172,12 +232,6 @@ public: 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, @@ -187,7 +241,15 @@ public: menuItem->onTrigger = onTrigger; menuItems.push_back(menuItem); - sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)&*menuItem, Widen(label).c_str())); + sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)menuItem.get(), Widen(label).c_str())); + + // uID is just an UINT, which isn't large enough to hold a pointer on 64-bit Windows, + // so we use dwItemData, which is. + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + mii.dwItemData = (LONG_PTR)menuItem.get(); + sscheck(SetMenuItemInfoW(hMenu, (UINT_PTR)menuItem.get(), FALSE, &mii)); return menuItem; } @@ -208,15 +270,17 @@ public: } void PopUp() override { + MENUINFO mi = {}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE; + mi.dwStyle = MNS_NOTIFYBYPOS; + sscheck(SetMenuInfo(hMenu, &mi)); + 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); - } + + sscheck(TrackPopupMenu(hMenu, TPM_TOPALIGN, pt.x, pt.y, 0, GetActiveWindow(), NULL)); + contextMenuPopTime = GetMilliseconds(); } void Clear() override { @@ -276,10 +340,6 @@ public: Clear(); sscheck(DestroyMenu(hMenuBar)); } - - void *NativePtr() override { - return hMenuBar; - } }; MenuBarRef GetOrCreateMainMenu(bool *unique) { @@ -287,5 +347,826 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) { return std::make_shared(); } +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +#define SCROLLBAR_UNIT 65536 + +class WindowImplWin32 : public Window { +public: + HWND hWindow = NULL; + HWND hTooltip = NULL; + HWND hEditor = NULL; + WNDPROC editorWndProc = NULL; + +#if HAVE_OPENGL == 1 + HGLRC hGlRc = NULL; +#elif HAVE_OPENGL == 3 + EGLDisplay eglDisplay = NULL; + EGLSurface eglSurface = NULL; + EGLContext eglContext = NULL; +#endif + + WINDOWPLACEMENT placement = {}; + int minWidth = 0, minHeight = 0; + + std::shared_ptr menuBar; + std::string tooltipText; + bool scrollbarVisible = false; + + static void RegisterWindowClass() { + static bool registered; + if(registered) return; + + WNDCLASSEX wc = {}; + wc.cbSize = sizeof(wc); + wc.style = CS_BYTEALIGNCLIENT|CS_BYTEALIGNWINDOW|CS_OWNDC|CS_DBLCLKS; + wc.lpfnWndProc = WndProc; + wc.cbWndExtra = sizeof(WindowImplWin32 *); + wc.hIcon = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000), + IMAGE_ICON, 32, 32, 0); + wc.hIconSm = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000), + IMAGE_ICON, 16, 16, 0); + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.lpszClassName = L"SolveSpace"; + sscheck(RegisterClassEx(&wc)); + registered = true; + } + + WindowImplWin32(Window::Kind kind, std::shared_ptr parentWindow) { + placement.length = sizeof(placement); + + RegisterWindowClass(); + + HWND hParentWindow = NULL; + if(parentWindow) { + hParentWindow = parentWindow->hWindow; + } + + DWORD style = WS_SIZEBOX|WS_CLIPCHILDREN; + switch(kind) { + case Window::Kind::TOPLEVEL: + style |= WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS; + break; + + case Window::Kind::TOOL: + style |= WS_POPUPWINDOW|WS_CAPTION; + break; + } + sscheck(hWindow = CreateWindowExW(0, L"SolveSpace", L"", style, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + hParentWindow, NULL, NULL, NULL)); + sscheck(SetWindowLongPtr(hWindow, 0, (LONG_PTR)this)); + if(hParentWindow != NULL) { + sscheck(SetWindowPos(hWindow, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE)); + } + + sscheck(hTooltip = CreateWindowExW(0, TOOLTIPS_CLASS, NULL, + WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + hWindow, NULL, NULL, NULL)); + sscheck(SetWindowPos(hTooltip, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE)); + + TOOLINFOW ti = {}; + ti.cbSize = sizeof(ti); + ti.uFlags = TTF_IDISHWND|TTF_SUBCLASS; + ti.hwnd = hWindow; + ti.uId = (UINT_PTR)hWindow; + ti.lpszText = (LPWSTR)L""; + sscheck(SendMessageW(hTooltip, TTM_ADDTOOLW, 0, (LPARAM)&ti)); + sscheck(SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0)); + + DWORD editorStyle = WS_CLIPSIBLINGS|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL; + sscheck(hEditor = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", editorStyle, + 0, 0, 0, 0, hWindow, NULL, NULL, NULL)); + sscheck(editorWndProc = + (WNDPROC)SetWindowLongPtr(hEditor, GWLP_WNDPROC, (LONG_PTR)EditorWndProc)); + + HDC hDc; + sscheck(hDc = GetDC(hWindow)); + +#if HAVE_OPENGL == 1 + PIXELFORMATDESCRIPTOR pfd = {}; + pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER; + pfd.dwLayerMask = PFD_MAIN_PLANE; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.cColorBits = 32; + pfd.cDepthBits = 24; + pfd.cAccumBits = 0; + pfd.cStencilBits = 0; + int pixelFormat; + sscheck(pixelFormat = ChoosePixelFormat(hDc, &pfd)); + sscheck(SetPixelFormat(hDc, pixelFormat, &pfd)); + + sscheck(hGlRc = wglCreateContext(hDc)); +#elif HAVE_OPENGL == 3 + ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API"); + + eglDisplay = eglGetDisplay(hDc); + ssassert(eglInitialize(eglDisplay, NULL, NULL), "Cannot initialize EGL"); + + EGLint configAttributes[] = { + EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_NONE + }; + EGLint numConfigs; + EGLConfig windowConfig; + ssassert(eglChooseConfig(eglDisplay, configAttributes, &windowConfig, 1, &numConfigs), + "Cannot choose EGL configuration"); + + EGLint surfaceAttributes[] = { + EGL_NONE + }; + eglSurface = eglCreateWindowSurface(eglDisplay, windowConfig, hWindow, surfaceAttributes); + ssassert(eglSurface != EGL_NO_SURFACE, "Cannot create EGL window surface"); + + EGLint contextAttributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + eglContext = eglCreateContext(eglDisplay, windowConfig, NULL, contextAttributes); + ssassert(eglContext != EGL_NO_CONTEXT, "Cannot create EGL context"); +#endif + + sscheck(ReleaseDC(hWindow, hDc)); + } + + static LRESULT CALLBACK WndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) { + if(handlingFatalError) return TRUE; + + WindowImplWin32 *window; + sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(h, 0)); + + // The wndproc may be called from within CreateWindowEx, and before we've associated + // the window with the WindowImplWin32. In that case, just defer to the default wndproc. + if(window == NULL) { + return DefWindowProc(h, msg, wParam, lParam); + } + + switch (msg) { + case WM_ERASEBKGND: + break; + + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hDc = BeginPaint(window->hWindow, &ps); + if(window->onRender) { +#if HAVE_OPENGL == 1 + wglMakeCurrent(hDc, window->hGlRc); +#elif HAVE_OPENGL == 3 + eglMakeCurrent(window->eglDisplay, window->eglSurface, + window->eglSurface, window->eglContext); +#endif + window->onRender(); +#if HAVE_OPENGL == 1 + SwapBuffers(hDc); +#elif HAVE_OPENGL == 3 + eglSwapBuffers(window->eglDisplay, window->eglSurface); + (void)hDc; +#endif + } + EndPaint(window->hWindow, &ps); + break; + } + + case WM_CLOSE: + case WM_DESTROY: + if(window->onClose) { + window->onClose(); + } + break; + + case WM_SIZE: + window->Invalidate(); + break; + + case WM_SIZING: { + int pixelRatio = window->GetDevicePixelRatio(); + + RECT rcw, rcc; + sscheck(GetWindowRect(window->hWindow, &rcw)); + sscheck(GetClientRect(window->hWindow, &rcc)); + int nonClientWidth = (rcw.right - rcw.left) - (rcc.right - rcc.left); + int nonClientHeight = (rcw.bottom - rcw.top) - (rcc.bottom - rcc.top); + + RECT *rc = (RECT *)lParam; + int adjWidth = rc->right - rc->left; + int adjHeight = rc->bottom - rc->top; + + adjWidth -= nonClientWidth; + adjWidth = max(window->minWidth * pixelRatio, adjWidth); + adjWidth += nonClientWidth; + adjHeight -= nonClientHeight; + adjHeight = max(window->minHeight * pixelRatio, adjHeight); + adjHeight += nonClientHeight; + switch(wParam) { + case WMSZ_RIGHT: + case WMSZ_BOTTOMRIGHT: + case WMSZ_TOPRIGHT: + rc->right = rc->left + adjWidth; + break; + + case WMSZ_LEFT: + case WMSZ_BOTTOMLEFT: + case WMSZ_TOPLEFT: + rc->left = rc->right - adjWidth; + break; + } + switch(wParam) { + case WMSZ_BOTTOM: + case WMSZ_BOTTOMLEFT: + case WMSZ_BOTTOMRIGHT: + rc->bottom = rc->top + adjHeight; + break; + + case WMSZ_TOP: + case WMSZ_TOPLEFT: + case WMSZ_TOPRIGHT: + rc->top = rc->bottom - adjHeight; + break; + } + break; + } + + case WM_DPICHANGED: { + RECT rc = *(RECT *)lParam; + sscheck(SendMessage(window->hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc)); + sscheck(SetWindowPos(window->hWindow, NULL, rc.left, rc.top, + rc.right - rc.left, rc.bottom - rc.top, + SWP_NOZORDER|SWP_NOACTIVATE)); + window->Invalidate(); + break; + } + + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_MBUTTONDBLCLK: + case WM_RBUTTONDBLCLK: + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + if(GetMilliseconds() - Platform::contextMenuPopTime < 100) { + // Ignore the mouse click that dismisses a context menu, to avoid + // (e.g.) clearing a selection. + return 0; + } + // fallthrough + case WM_MOUSEMOVE: + case WM_MOUSEWHEEL: + case WM_MOUSELEAVE: { + int pixelRatio = window->GetDevicePixelRatio(); + + MouseEvent event = {}; + event.x = GET_X_LPARAM(lParam) / pixelRatio; + event.y = GET_Y_LPARAM(lParam) / pixelRatio; + event.button = MouseEvent::Button::NONE; + + event.shiftDown = (wParam & MK_SHIFT) != 0; + event.controlDown = (wParam & MK_CONTROL) != 0; + + switch(msg) { + case WM_LBUTTONDOWN: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::PRESS; + break; + case WM_MBUTTONDOWN: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::PRESS; + break; + case WM_RBUTTONDOWN: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::PRESS; + break; + + case WM_LBUTTONDBLCLK: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::DBL_PRESS; + break; + case WM_MBUTTONDBLCLK: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::DBL_PRESS; + break; + case WM_RBUTTONDBLCLK: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::DBL_PRESS; + break; + + case WM_LBUTTONUP: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::RELEASE; + break; + case WM_MBUTTONUP: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::RELEASE; + break; + case WM_RBUTTONUP: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::RELEASE; + break; + + case WM_MOUSEWHEEL: + // Make the mousewheel work according to which window the mouse is + // over, not according to which window is active. + POINT pt; + pt.x = LOWORD(lParam); + pt.y = HIWORD(lParam); + HWND hWindowUnderMouse; + sscheck(hWindowUnderMouse = WindowFromPoint(pt)); + if(hWindowUnderMouse && hWindowUnderMouse != h) { + SendMessageW(hWindowUnderMouse, msg, wParam, lParam); + break; + } + + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) > 0 ? 1 : -1; + break; + + case WM_MOUSELEAVE: + event.type = MouseEvent::Type::LEAVE; + break; + case WM_MOUSEMOVE: { + event.type = MouseEvent::Type::MOTION; + + if(wParam & MK_LBUTTON) { + event.button = MouseEvent::Button::LEFT; + } else if(wParam & MK_MBUTTON) { + event.button = MouseEvent::Button::MIDDLE; + } else if(wParam & MK_RBUTTON) { + event.button = MouseEvent::Button::RIGHT; + } + + // We need this in order to get the WM_MOUSELEAVE + TRACKMOUSEEVENT tme = {}; + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = window->hWindow; + sscheck(TrackMouseEvent(&tme)); + break; + } + } + + if(window->onMouseEvent) { + window->onMouseEvent(event); + } + break; + } + + case WM_KEYDOWN: + case WM_KEYUP: { + Platform::KeyboardEvent event = {}; + if(msg == WM_KEYDOWN) { + event.type = Platform::KeyboardEvent::Type::PRESS; + } else if(msg == WM_KEYUP) { + event.type = Platform::KeyboardEvent::Type::RELEASE; + } + + if(GetKeyState(VK_SHIFT) & 0x8000) + event.shiftDown = true; + if(GetKeyState(VK_CONTROL) & 0x8000) + event.controlDown = true; + + 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. + break; + } + } else if(event.chr == '.' && event.shiftDown) { + event.chr = '>'; + event.shiftDown = false;; + } + } + + if(window->onKeyboardEvent) { + window->onKeyboardEvent(event); + } else { + HWND hParent; + sscheck(hParent = GetParent(h)); + if(hParent != NULL) { + sscheck(SetForegroundWindow(hParent)); + sscheck(SendMessageW(hParent, msg, wParam, lParam)); + } + } + break; + } + + case WM_SYSKEYDOWN: { + HWND hParent; + sscheck(hParent = GetParent(h)); + if(hParent != NULL) { + // If the user presses the Alt key when a tool window has focus, + // then that should probably go to the main window instead. + sscheck(SetForegroundWindow(hParent)); + break; + } else { + return DefWindowProc(h, msg, wParam, lParam); + } + } + + case WM_VSCROLL: { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS|SIF_TRACKPOS|SIF_RANGE|SIF_PAGE; + sscheck(GetScrollInfo(window->hWindow, SB_VERT, &si)); + + switch(LOWORD(wParam)) { + case SB_LINEUP: si.nPos -= SCROLLBAR_UNIT; break; + case SB_PAGEUP: si.nPos -= si.nPage; break; + case SB_LINEDOWN: si.nPos += SCROLLBAR_UNIT; break; + case SB_PAGEDOWN: si.nPos += si.nPage; break; + case SB_TOP: si.nPos = si.nMin; break; + case SB_BOTTOM: si.nPos = si.nMax; break; + case SB_THUMBTRACK: + case SB_THUMBPOSITION: si.nPos = si.nTrackPos; break; + } + + si.nPos = min((UINT)si.nPos, (UINT)(si.nMax - si.nPage)); + + if(window->onScrollbarAdjusted) { + window->onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT); + } + break; + } + + case WM_MENUCOMMAND: { + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + sscheck(GetMenuItemInfoW((HMENU)lParam, wParam, TRUE, &mii)); + + MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)mii.dwItemData; + if(menuItem->onTrigger) { + menuItem->onTrigger(); + } + break; + } + + default: + return DefWindowProc(h, msg, wParam, lParam); + } + + return 0; + } + + static LRESULT CALLBACK EditorWndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) { + if(handlingFatalError) return 0; + + HWND hWindow; + sscheck(hWindow = GetParent(h)); + + WindowImplWin32 *window; + sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(hWindow, 0)); + + switch(msg) { + case WM_KEYDOWN: + if(wParam == VK_RETURN) { + if(window->onEditingDone) { + int length; + sscheck(length = GetWindowTextLength(h)); + + std::wstring resultW; + resultW.resize(length); + sscheck(GetWindowTextW(h, &resultW[0], resultW.length() + 1)); + + window->onEditingDone(Narrow(resultW)); + return 0; + } + } else if(wParam == VK_ESCAPE) { + sscheck(SendMessageW(hWindow, msg, wParam, lParam)); + return 0; + } + } + + return CallWindowProc(window->editorWndProc, h, msg, wParam, lParam); + } + + double GetPixelDensity() override { + UINT dpi; + sscheck(dpi = ssGetDpiForWindow(hWindow)); + return (double)dpi; + } + + int GetDevicePixelRatio() override { + UINT dpi; + sscheck(dpi = ssGetDpiForWindow(hWindow)); + return dpi / USER_DEFAULT_SCREEN_DPI; + } + + bool IsVisible() override { + BOOL isVisible; + sscheck(isVisible = IsWindowVisible(hWindow)); + return isVisible == TRUE; + } + + void SetVisible(bool visible) override { + sscheck(ShowWindow(hWindow, visible ? SW_SHOW : SW_HIDE)); + } + + void Focus() override { + sscheck(SetActiveWindow(hWindow)); + } + + bool IsFullScreen() override { + DWORD style; + sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE)); + return !(style & WS_OVERLAPPEDWINDOW); + } + + void SetFullScreen(bool fullScreen) override { + DWORD style; + sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE)); + if(fullScreen) { + sscheck(GetWindowPlacement(hWindow, &placement)); + + MONITORINFO mi; + mi.cbSize = sizeof(mi); + sscheck(GetMonitorInfo(MonitorFromWindow(hWindow, MONITOR_DEFAULTTONEAREST), &mi)); + + sscheck(SetWindowLong(hWindow, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW)); + sscheck(SetWindowPos(hWindow, HWND_TOP, + mi.rcMonitor.left, mi.rcMonitor.top, + mi.rcMonitor.right - mi.rcMonitor.left, + mi.rcMonitor.bottom - mi.rcMonitor.top, + SWP_NOOWNERZORDER|SWP_FRAMECHANGED)); + } else { + sscheck(SetWindowLong(hWindow, GWL_STYLE, style | WS_OVERLAPPEDWINDOW)); + sscheck(SetWindowPlacement(hWindow, &placement)); + sscheck(SetWindowPos(hWindow, NULL, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER| + SWP_NOOWNERZORDER|SWP_FRAMECHANGED)); + } + } + + void SetTitle(const std::string &title) override { + sscheck(SetWindowTextW(hWindow, Title(title).c_str())); + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + menuBar = std::static_pointer_cast(newMenuBar); + + MENUINFO mi = {}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE; + mi.dwStyle = MNS_NOTIFYBYPOS; + sscheck(SetMenuInfo(menuBar->hMenuBar, &mi)); + + sscheck(SetMenu(hWindow, menuBar->hMenuBar)); + } + + void GetContentSize(double *width, double *height) override { + int pixelRatio = GetDevicePixelRatio(); + + RECT rc; + sscheck(GetClientRect(hWindow, &rc)); + *width = (rc.right - rc.left) / pixelRatio; + *height = (rc.bottom - rc.top) / pixelRatio; + } + + void SetMinContentSize(double width, double height) { + minWidth = (int)width; + minHeight = (int)height; + + int pixelRatio = GetDevicePixelRatio(); + + RECT rc; + sscheck(GetClientRect(hWindow, &rc)); + if(rc.right - rc.left < minWidth * pixelRatio) { + rc.right = rc.left + minWidth * pixelRatio; + } + if(rc.bottom - rc.top < minHeight * pixelRatio) { + rc.bottom = rc.top + minHeight * pixelRatio; + } + } + + void FreezePosition(const std::string &key) override { + sscheck(GetWindowPlacement(hWindow, &placement)); + + BOOL isMaximized; + sscheck(isMaximized = IsZoomed(hWindow)); + + RECT rc = placement.rcNormalPosition; + CnfFreezeInt(rc.left, key + "_left"); + CnfFreezeInt(rc.right, key + "_right"); + CnfFreezeInt(rc.top, key + "_top"); + CnfFreezeInt(rc.bottom, key + "_bottom"); + CnfFreezeInt(isMaximized, key + "_maximized"); + } + + void ThawPosition(const std::string &key) override { + sscheck(GetWindowPlacement(hWindow, &placement)); + + RECT rc = placement.rcNormalPosition; + rc.left = CnfThawInt(rc.left, key + "_left"); + rc.right = CnfThawInt(rc.right, key + "_right"); + rc.top = CnfThawInt(rc.top, key + "_top"); + rc.bottom = CnfThawInt(rc.bottom, key + "_bottom"); + + MONITORINFO mi; + mi.cbSize = sizeof(mi); + sscheck(GetMonitorInfo(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), &mi)); + + // If it somehow ended up off-screen, then put it back. + RECT mrc = mi.rcMonitor; + rc.left = Clamp(rc.left, mrc.left, mrc.right); + rc.right = Clamp(rc.right, mrc.left, mrc.right); + rc.top = Clamp(rc.top, mrc.top, mrc.bottom); + rc.bottom = Clamp(rc.bottom, mrc.top, mrc.bottom); + + // And make sure the minimum size is respected. (We can freeze a size smaller + // than minimum size if the DPI changed between runs.) + sscheck(SendMessageW(hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc)); + + placement.flags = 0; + if(CnfThawInt(false, key + "_maximized")) { + placement.showCmd = SW_SHOWMAXIMIZED; + } else { + placement.showCmd = SW_SHOW; + } + placement.rcNormalPosition = rc; + sscheck(SetWindowPlacement(hWindow, &placement)); + } + + void SetCursor(Cursor cursor) override { + LPWSTR cursorName; + switch(cursor) { + case Cursor::POINTER: cursorName = IDC_ARROW; break; + case Cursor::HAND: cursorName = IDC_HAND; break; + } + + HCURSOR hCursor; + sscheck(hCursor = LoadCursorW(NULL, cursorName)); + sscheck(::SetCursor(hCursor)); + } + + void SetTooltip(const std::string &newText) override { + // The following SendMessage calls sometimes fail with ERROR_ACCESS_DENIED for + // no discernible reason, but only on wine. + if(newText.empty()) { + SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0); + SendMessageW(hTooltip, TTM_POP, 0, 0); + } else if(newText != tooltipText) { + tooltipText = newText; + + std::wstring newTextW = Widen(newText); + TOOLINFOW ti = {}; + ti.cbSize = sizeof(ti); + ti.uFlags = TTF_IDISHWND; + ti.hwnd = hWindow; + ti.uId = (UINT_PTR)hWindow; + ti.lpszText = &newTextW[0]; + SendMessageW(hTooltip, TTM_UPDATETIPTEXTW, 0, (LPARAM)&ti); + + SendMessageW(hTooltip, TTM_ACTIVATE, TRUE, 0); + SendMessageW(hTooltip, TTM_POPUP, 0, 0); + } + } + + bool IsEditorVisible() override { + BOOL visible; + sscheck(visible = IsWindowVisible(hEditor)); + return visible == TRUE; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + if(IsEditorVisible()) return; + + int pixelRatio = GetDevicePixelRatio(); + + HFONT hFont = CreateFontW(-(LONG)fontHeight * GetDevicePixelRatio(), 0, 0, 0, + FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial"); + if(hFont == NULL) { + sscheck(hFont = (HFONT)GetStockObject(SYSTEM_FONT)); + } + sscheck(SendMessageW(hEditor, WM_SETFONT, (WPARAM)hFont, FALSE)); + sscheck(SendMessageW(hEditor, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, 0)); + + std::wstring textW = Widen(text); + + HDC hDc; + TEXTMETRICW tm; + SIZE ts; + sscheck(hDc = GetDC(hEditor)); + sscheck(SelectObject(hDc, hFont)); + sscheck(GetTextMetricsW(hDc, &tm)); + sscheck(GetTextExtentPoint32W(hDc, textW.c_str(), textW.length(), &ts)); + sscheck(ReleaseDC(hEditor, hDc)); + + RECT rc; + rc.left = (LONG)x * pixelRatio; + rc.top = (LONG)y * pixelRatio - tm.tmAscent; + // Add one extra char width to avoid scrolling. + rc.right = (LONG)x * pixelRatio + + std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth); + rc.bottom = (LONG)y * pixelRatio + tm.tmDescent; + sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE, + ssGetDpiForWindow(hWindow))); + + sscheck(MoveWindow(hEditor, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, + /*bRepaint=*/true)); + sscheck(ShowWindow(hEditor, SW_SHOW)); + if(!textW.empty()) { + sscheck(SendMessageW(hEditor, WM_SETTEXT, 0, (LPARAM)textW.c_str())); + sscheck(SendMessageW(hEditor, EM_SETSEL, 0, textW.length())); + sscheck(SetFocus(hEditor)); + } + } + + void HideEditor() override { + if(!IsEditorVisible()) return; + + sscheck(ShowWindow(hEditor, SW_HIDE)); + } + + void SetScrollbarVisible(bool visible) override { + scrollbarVisible = visible; + sscheck(ShowScrollBar(hWindow, SB_VERT, visible)); + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE|SIF_PAGE; + si.nMin = (UINT)(min * SCROLLBAR_UNIT); + si.nMax = (UINT)(max * SCROLLBAR_UNIT); + si.nPage = (UINT)(pageSize * SCROLLBAR_UNIT); + sscheck(SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE)); + } + + double GetScrollbarPosition() override { + if(!scrollbarVisible) return 0.0; + + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + sscheck(GetScrollInfo(hWindow, SB_VERT, &si)); + return (double)si.nPos / SCROLLBAR_UNIT; + } + + void SetScrollbarPosition(double pos) override { + if(!scrollbarVisible) return; + + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + si.nPos = (UINT)(pos * SCROLLBAR_UNIT); + sscheck(SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE)); + + // Windows won't synthesize a WM_VSCROLL for us here. + if(onScrollbarAdjusted) { + onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT); + } + } + + void Invalidate() override { + sscheck(InvalidateRect(hWindow, NULL, /*bErase=*/FALSE)); + } + + void Redraw() override { + Invalidate(); + sscheck(UpdateWindow(hWindow)); + } + + void *NativePtr() override { + return hWindow; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + PostQuitMessage(0); +} + } } diff --git a/src/platform/w32main.cpp b/src/platform/w32main.cpp index d18f4b0d..da7058a7 100644 --- a/src/platform/w32main.cpp +++ b/src/platform/w32main.cpp @@ -16,53 +16,15 @@ #include #include -#ifdef MenuHelp -// This is defined to IsolationAwareMenuHelp on Windows 6.0 and later. -#undef MenuHelp -#endif - #ifdef HAVE_SPACEWARE # include # include # undef uint32_t // thanks but no thanks #endif -#if HAVE_OPENGL == 3 -#define EGLAPI /*static linkage*/ -#include -#endif - using Platform::Narrow; using Platform::Widen; -HINSTANCE Instance; - -HWND TextWnd; -HWND TextWndScrollBar; -HWND TextEditControl; -#if HAVE_OPENGL == 3 -EGLDisplay TextGlDisplay; -EGLSurface TextGlSurface; -EGLContext TextGlContext; -#else -HGLRC TextGl; -#endif - -HWND GraphicsWnd; -HWND GraphicsEditControl; -#if HAVE_OPENGL == 3 -EGLDisplay GraphicsGlDisplay; -EGLSurface GraphicsGlSurface; -EGLContext GraphicsGlContext; -#else -HGLRC GraphicsGl; -#endif -static struct { - int x, y; -} LastMousePos; - -int ClientIsSmallerBy; - HFONT FixedFont; #ifdef HAVE_SPACEWARE @@ -143,42 +105,42 @@ HWND CreateWindowClient(DWORD exStyle, const wchar_t *className, const wchar_t * void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); // Register the window class for our dialog. WNDCLASSEX wc = {}; wc.cbSize = sizeof(wc); wc.style = CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_OWNDC; wc.lpfnWndProc = (WNDPROC)MessageProc; - wc.hInstance = Instance; + wc.hInstance = NULL; wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW; wc.lpszClassName = L"MessageWnd"; wc.lpszMenuName = NULL; wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hIcon = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), + wc.hIcon = (HICON)LoadImage(NULL, MAKEINTRESOURCE(4000), IMAGE_ICON, 32, 32, 0); - wc.hIconSm = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), + wc.hIconSm = (HICON)LoadImage(NULL, MAKEINTRESOURCE(4000), IMAGE_ICON, 16, 16, 0); RegisterClassEx(&wc); // Create the window. MessageString = str; RECT r; - GetWindowRect(GraphicsWnd, &r); + GetWindowRect((HWND)SS.GW.window->NativePtr(), &r); int width = cols*SS.TW.CHAR_WIDTH_ + 20, height = rows*SS.TW.LINE_HEIGHT + 60; MessageWidth = width; MessageHeight = height; MessageWnd = CreateWindowClient(0, L"MessageWnd", - (error ? Title(C_("title", "Error")) : Title(C_("title", "Message"))).c_str(), + Title(error ? C_("title", "Error") : C_("title", "Message")).c_str(), WS_OVERLAPPED | WS_SYSMENU, - r.left + 100, r.top + 100, width, height, NULL, NULL, Instance, NULL); + r.left + 100, r.top + 100, width, height, NULL, NULL, NULL, NULL); OkButton = CreateWindowExW(0, WC_BUTTON, Widen(C_("button", "OK")).c_str(), WS_CHILD | WS_TABSTOP | WS_CLIPSIBLINGS | WS_VISIBLE | BS_DEFPUSHBUTTON, (width - 70)/2, rows*SS.TW.LINE_HEIGHT + 20, - 70, 25, MessageWnd, NULL, Instance, NULL); + 70, 25, MessageWnd, NULL, NULL, NULL); SendMessage(OkButton, WM_SETFONT, (WPARAM)FixedFont, true); ShowWindow(MessageWnd, true); @@ -203,53 +165,22 @@ void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) } MessageString = NULL; - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); DestroyWindow(MessageWnd); -} -static void GetWindowSize(HWND hwnd, int *w, int *h) -{ - RECT r; - GetClientRect(hwnd, &r); - *w = r.right - r.left; - *h = r.bottom - r.top; -} -void SolveSpace::GetGraphicsWindowSize(int *w, int *h) -{ - GetWindowSize(GraphicsWnd, w, h); -} -void SolveSpace::GetTextWindowSize(int *w, int *h) -{ - GetWindowSize(TextWnd, w, h); -} - -double SolveSpace::GetScreenDpi() { - HDC hdc = GetDC(NULL); - double dpi = GetDeviceCaps(hdc, LOGPIXELSX); - ReleaseDC(NULL, hdc); - return dpi; + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); } void SolveSpace::OpenWebsite(const char *url) { - ShellExecuteW(GraphicsWnd, L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); -} - -void SolveSpace::ExitNow() { - PostQuitMessage(0); + ShellExecuteW((HWND)SS.GW.window->NativePtr(), + L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); } //----------------------------------------------------------------------------- // Helpers so that we can read/write registry keys from the platform- // independent code. //----------------------------------------------------------------------------- -inline int CLAMP(int v, int a, int b) { - // Clamp it to the range [a, b] - if(v <= a) return a; - if(v >= b) return b; - return v; -} static HKEY GetRegistryKey() { @@ -292,17 +223,6 @@ void SolveSpace::CnfFreezeString(const std::string &str, const std::string &name REG_SZ, (const BYTE*) &strW[0], (strW.length() + 1) * 2); RegCloseKey(SolveSpace); } -static void FreezeWindowPos(HWND hwnd, const std::string &name) -{ - RECT r; - GetWindowRect(hwnd, &r); - CnfFreezeInt(r.left, name + "_left"); - CnfFreezeInt(r.right, name + "_right"); - CnfFreezeInt(r.top, name + "_top"); - CnfFreezeInt(r.bottom, name + "_bottom"); - - CnfFreezeInt(IsZoomed(hwnd), name + "_maximized"); -} uint32_t SolveSpace::CnfThawInt(uint32_t val, const std::string &name) { @@ -352,613 +272,6 @@ std::string SolveSpace::CnfThawString(const std::string &val, const std::string RegCloseKey(SolveSpace); return Narrow(newval); } -static void ThawWindowPos(HWND hwnd, const std::string &name) -{ - RECT r; - GetWindowRect(hwnd, &r); - r.left = CnfThawInt(r.left, name + "_left"); - r.right = CnfThawInt(r.right, name + "_right"); - r.top = CnfThawInt(r.top, name + "_top"); - r.bottom = CnfThawInt(r.bottom, name + "_bottom"); - - HMONITOR hMonitor = MonitorFromRect(&r, MONITOR_DEFAULTTONEAREST);; - MONITORINFO mi; - mi.cbSize = sizeof(mi); - GetMonitorInfo(hMonitor, &mi); - - // If it somehow ended up off-screen, then put it back. - RECT dr = mi.rcMonitor; - r.left = CLAMP(r.left, dr.left, dr.right); - r.right = CLAMP(r.right, dr.left, dr.right); - r.top = CLAMP(r.top, dr.top, dr.bottom); - r.bottom = CLAMP(r.bottom, dr.top, dr.bottom); - MoveWindow(hwnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE); - - if(CnfThawInt(FALSE, name + "_maximized")) - ShowWindow(hwnd, SW_MAXIMIZE); -} - -void SolveSpace::SetCurrentFilename(const Platform::Path &filename) { - SetWindowTextW(GraphicsWnd, - Title(filename.IsEmpty() ? C_("title", "(new sketch)") : filename.raw).c_str()); -} - -void SolveSpace::SetMousePointerToHand(bool yes) { - SetCursor(LoadCursor(NULL, yes ? IDC_HAND : IDC_ARROW)); -} - -static void PaintTextWnd() -{ -#if HAVE_OPENGL == 3 - eglMakeCurrent(TextGlDisplay, TextGlSurface, TextGlSurface, TextGlContext); - - SS.TW.Paint(); - eglSwapBuffers(TextGlDisplay, TextGlSurface); - - // Leave the graphics window context active, except when we're painting - // this text window. - eglMakeCurrent(GraphicsGlDisplay, GraphicsGlSurface, GraphicsGlSurface, GraphicsGlContext); -#else - wglMakeCurrent(GetDC(TextWnd), TextGl); - - SS.TW.Paint(); - SwapBuffers(GetDC(TextWnd)); - - // Leave the graphics window context active, except when we're painting - // this text window. - wglMakeCurrent(GetDC(GraphicsWnd), GraphicsGl); -#endif -} - -void SolveSpace::MoveTextScrollbarTo(int pos, int maxPos, int page) -{ - SCROLLINFO si = {}; - si.cbSize = sizeof(si); - si.fMask = SIF_DISABLENOSCROLL | SIF_ALL; - si.nMin = 0; - si.nMax = maxPos; - si.nPos = pos; - si.nPage = page; - SetScrollInfo(TextWndScrollBar, SB_CTL, &si, true); -} - -void HandleTextWindowScrollBar(WPARAM wParam, LPARAM lParam) -{ - int maxPos, minPos, pos; - GetScrollRange(TextWndScrollBar, SB_CTL, &minPos, &maxPos); - pos = GetScrollPos(TextWndScrollBar, SB_CTL); - - switch(LOWORD(wParam)) { - case SB_LINEUP: pos--; break; - case SB_PAGEUP: pos -= 4; break; - - case SB_LINEDOWN: pos++; break; - case SB_PAGEDOWN: pos += 4; break; - - case SB_TOP: pos = 0; break; - - case SB_BOTTOM: pos = maxPos; break; - - case SB_THUMBTRACK: - case SB_THUMBPOSITION: pos = HIWORD(wParam); break; - } - - SS.TW.ScrollbarEvent(pos); -} - -static void MouseWheel(int thisDelta) { - static int DeltaAccum; - int delta = 0; - // Handle mouse deltas of less than 120 (like from an un-detented mouse - // wheel) correctly, even though no one ever uses those. - DeltaAccum += thisDelta; - while(DeltaAccum >= 120) { - DeltaAccum -= 120; - delta += 120; - } - while(DeltaAccum <= -120) { - DeltaAccum += 120; - delta -= 120; - } - if(delta == 0) return; - - POINT pt; - GetCursorPos(&pt); - HWND hw = WindowFromPoint(pt); - - // Make the mousewheel work according to which window the mouse is - // over, not according to which window is active. - bool inTextWindow; - if(hw == TextWnd) { - inTextWindow = true; - } else if(hw == GraphicsWnd) { - inTextWindow = false; - } else if(GetForegroundWindow() == TextWnd) { - inTextWindow = true; - } else { - inTextWindow = false; - } - - if(inTextWindow) { - int i; - for(i = 0; i < abs(delta/40); i++) { - HandleTextWindowScrollBar(delta > 0 ? SB_LINEUP : SB_LINEDOWN, 0); - } - } else { - SS.GW.MouseScroll(LastMousePos.x, LastMousePos.y, delta); - } -} - -LRESULT CALLBACK TextWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if(Platform::handlingFatalError) return 1; - - switch (msg) { - case WM_ERASEBKGND: - break; - - case WM_CLOSE: - case WM_DESTROY: - SolveSpaceUI::MenuFile(Command::EXIT); - break; - - case WM_PAINT: { - // Actually paint the text window, with gl. - PaintTextWnd(); - // And then just make Windows happy. - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - EndPaint(hwnd, &ps); - break; - } - - case WM_SIZING: { - RECT *r = (RECT *)lParam; - int hc = (r->bottom - r->top) - ClientIsSmallerBy; - int extra = hc % (SS.TW.LINE_HEIGHT/2); - switch(wParam) { - case WMSZ_BOTTOM: - case WMSZ_BOTTOMLEFT: - case WMSZ_BOTTOMRIGHT: - r->bottom -= extra; - break; - - case WMSZ_TOP: - case WMSZ_TOPLEFT: - case WMSZ_TOPRIGHT: - r->top += extra; - break; - } - int tooNarrow = (SS.TW.MIN_COLS*SS.TW.CHAR_WIDTH_) - - (r->right - r->left); - if(tooNarrow >= 0) { - switch(wParam) { - case WMSZ_RIGHT: - case WMSZ_BOTTOMRIGHT: - case WMSZ_TOPRIGHT: - r->right += tooNarrow; - break; - - case WMSZ_LEFT: - case WMSZ_BOTTOMLEFT: - case WMSZ_TOPLEFT: - r->left -= tooNarrow; - break; - } - } - break; - } - - case WM_MOUSELEAVE: - SS.TW.MouseLeave(); - break; - - case WM_LBUTTONDOWN: - case WM_MOUSEMOVE: { - // We need this in order to get the WM_MOUSELEAVE - TRACKMOUSEEVENT tme = {}; - tme.cbSize = sizeof(tme); - tme.dwFlags = TME_LEAVE; - tme.hwndTrack = TextWnd; - TrackMouseEvent(&tme); - - // And process the actual message - int x = LOWORD(lParam); - int y = HIWORD(lParam); - SS.TW.MouseEvent(msg == WM_LBUTTONDOWN, wParam & MK_LBUTTON, x, y); - break; - } - - case WM_SIZE: { - RECT r; - GetWindowRect(TextWndScrollBar, &r); - int sw = r.right - r.left; - GetClientRect(hwnd, &r); - MoveWindow(TextWndScrollBar, r.right - sw, r.top, sw, - (r.bottom - r.top), true); - // If the window is growing, then the scrollbar position may - // be moving, so it's as if we're dragging the scrollbar. - HandleTextWindowScrollBar((WPARAM)-1, -1); - InvalidateRect(TextWnd, NULL, false); - break; - } - - case WM_MOUSEWHEEL: - MouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); - break; - - case WM_VSCROLL: - HandleTextWindowScrollBar(wParam, lParam); - break; - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - - return 1; -} - -static std::string EditControlText(HWND hwnd) -{ - std::wstring result; - result.resize(GetWindowTextLength(hwnd)); - GetWindowTextW(hwnd, &result[0], result.length() + 1); - return Narrow(result); -} - -static bool ProcessKeyDown(WPARAM wParam) -{ - if(GraphicsEditControlIsVisible() && wParam != VK_ESCAPE) { - if(wParam == VK_RETURN) { - SS.GW.EditControlDone(EditControlText(GraphicsEditControl).c_str()); - return true; - } else { - return false; - } - } - if(TextEditControlIsVisible() && wParam != VK_ESCAPE) { - if(wParam == VK_RETURN) { - SS.TW.EditControlDone(EditControlText(TextEditControl).c_str()); - } else { - return false; - } - } - - Platform::KeyboardEvent event = {}; - event.type = Platform::KeyboardEvent::Type::PRESS; - - if(GetAsyncKeyState(VK_SHIFT) & 0x8000) - event.shiftDown = true; - if(GetAsyncKeyState(VK_CONTROL) & 0x8000) - event.controlDown = true; - - 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.KeyboardEvent(event)) return true; - - return false; -} - -void SolveSpace::ShowTextWindow(bool visible) -{ - ShowWindow(TextWnd, visible ? SW_SHOWNOACTIVATE : SW_HIDE); -} - -#if HAVE_OPENGL == 3 -static void CreateGlContext(HWND hwnd, EGLDisplay *eglDisplay, EGLSurface *eglSurface, - EGLContext *eglContext) { - ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API"); - - *eglDisplay = eglGetDisplay(GetDC(hwnd)); - ssassert(eglInitialize(*eglDisplay, NULL, NULL), "Cannot initialize EGL"); - - EGLint configAttributes[] = { - EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_DEPTH_SIZE, 24, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_NONE - }; - EGLint numConfigs; - EGLConfig windowConfig; - ssassert(eglChooseConfig(*eglDisplay, configAttributes, &windowConfig, 1, &numConfigs), - "Cannot choose EGL configuration"); - - EGLint surfaceAttributes[] = { - EGL_NONE - }; - *eglSurface = eglCreateWindowSurface(*eglDisplay, windowConfig, hwnd, surfaceAttributes); - ssassert(eglSurface != EGL_NO_SURFACE, "Cannot create EGL window surface"); - - EGLint contextAttributes[] = { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - *eglContext = eglCreateContext(*eglDisplay, windowConfig, NULL, contextAttributes); - ssassert(eglContext != EGL_NO_CONTEXT, "Cannot create EGL context"); - - eglMakeCurrent(*eglDisplay, *eglSurface, *eglSurface, *eglContext); -} -#else -static void CreateGlContext(HWND hwnd, HGLRC *glrc) -{ - HDC hdc = GetDC(hwnd); - - PIXELFORMATDESCRIPTOR pfd = {}; - int pixelFormat; - - pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); - pfd.nVersion = 1; - pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | - PFD_DOUBLEBUFFER; - pfd.dwLayerMask = PFD_MAIN_PLANE; - pfd.iPixelType = PFD_TYPE_RGBA; - pfd.cColorBits = 32; - pfd.cDepthBits = 24; - pfd.cAccumBits = 0; - pfd.cStencilBits = 0; - - pixelFormat = ChoosePixelFormat(hdc, &pfd); - ssassert(pixelFormat != 0, "Expected a valid pixel format to be chosen"); - - ssassert(SetPixelFormat(hdc, pixelFormat, &pfd), "Cannot set pixel format"); - - *glrc = wglCreateContext(hdc); - wglMakeCurrent(hdc, *glrc); -} -#endif - -void SolveSpace::PaintGraphics() -{ - SS.GW.Paint(); -#if HAVE_OPENGL == 3 - eglSwapBuffers(GraphicsGlDisplay, GraphicsGlSurface); -#else - SwapBuffers(GetDC(GraphicsWnd)); -#endif -} -void SolveSpace::InvalidateGraphics() -{ - InvalidateRect(GraphicsWnd, NULL, false); -} - -void SolveSpace::ToggleFullScreen() -{ - static WINDOWPLACEMENT wp; - wp.length = sizeof(wp); - - DWORD dwStyle = GetWindowLong(GraphicsWnd, GWL_STYLE); - if(dwStyle & WS_OVERLAPPEDWINDOW) { - MONITORINFO mi; - mi.cbSize = sizeof(mi); - - if(GetWindowPlacement(GraphicsWnd, &wp) && - GetMonitorInfo(MonitorFromWindow(GraphicsWnd, MONITOR_DEFAULTTOPRIMARY), &mi)) { - SetWindowLong(GraphicsWnd, GWL_STYLE, dwStyle & ~WS_OVERLAPPEDWINDOW); - SetWindowPos(GraphicsWnd, HWND_TOP, - mi.rcMonitor.left, mi.rcMonitor.top, - mi.rcMonitor.right - mi.rcMonitor.left, - mi.rcMonitor.bottom - mi.rcMonitor.top, - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - } - } else { - SetWindowLong(GraphicsWnd, GWL_STYLE, dwStyle | WS_OVERLAPPEDWINDOW); - SetWindowPlacement(GraphicsWnd, &wp); - SetWindowPos(GraphicsWnd, NULL, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - } -} -bool SolveSpace::FullScreenIsActive() -{ - return (GetWindowLong(GraphicsWnd, GWL_STYLE) & WS_OVERLAPPEDWINDOW) != 0; -} - -void SolveSpace::InvalidateText() -{ - InvalidateRect(TextWnd, NULL, false); -} - -static void ShowEditControl(HWND h, int x, int y, int fontHeight, int minWidthChars, - bool isMonospace, const std::wstring &s) { - static HFONT hf; - if(hf) DeleteObject(hf); - hf = CreateFontW(-fontHeight, 0, 0, 0, - FW_REGULAR, false, false, false, ANSI_CHARSET, - OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, - DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial"); - if(hf) SendMessage(h, WM_SETFONT, (WPARAM)hf, false); - else SendMessage(h, WM_SETFONT, (WPARAM)(HFONT)GetStockObject(SYSTEM_FONT), false); - SendMessage(h, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, 0); - - HDC hdc = GetDC(h); - TEXTMETRICW tm; - SIZE ts; - SelectObject(hdc, hf); - GetTextMetrics(hdc, &tm); - GetTextExtentPoint32W(hdc, s.c_str(), s.length(), &ts); - ReleaseDC(h, hdc); - - RECT rc; - rc.left = x; - rc.top = y - tm.tmAscent; - // Add one extra char width to avoid scrolling. - rc.right = x + std::max(tm.tmAveCharWidth * minWidthChars, - ts.cx + tm.tmAveCharWidth); - rc.bottom = y + tm.tmDescent; - - AdjustWindowRectEx(&rc, 0, false, WS_EX_CLIENTEDGE); - MoveWindow(h, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, true); - ShowWindow(h, SW_SHOW); - if(!s.empty()) { - SendMessage(h, WM_SETTEXT, 0, (LPARAM)s.c_str()); - SendMessage(h, EM_SETSEL, 0, s.length()); - SetFocus(h); - } -} -void SolveSpace::ShowTextEditControl(int x, int y, const std::string &str) -{ - if(GraphicsEditControlIsVisible()) return; - - ShowEditControl(TextEditControl, x, y, TextWindow::CHAR_HEIGHT, 30, - /*isMonospace=*/true, Widen(str)); -} -void SolveSpace::HideTextEditControl() -{ - ShowWindow(TextEditControl, SW_HIDE); -} -bool SolveSpace::TextEditControlIsVisible() -{ - return IsWindowVisible(TextEditControl) ? true : false; -} -void SolveSpace::ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &str) -{ - if(GraphicsEditControlIsVisible()) return; - - RECT r; - GetClientRect(GraphicsWnd, &r); - x = x + (r.right - r.left)/2; - y = (r.bottom - r.top)/2 - y; - - ShowEditControl(GraphicsEditControl, x, y, fontHeight, minWidthChars, - /*isMonospace=*/false, Widen(str)); -} -void SolveSpace::HideGraphicsEditControl() -{ - ShowWindow(GraphicsEditControl, SW_HIDE); -} -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) -{ - if(Platform::handlingFatalError) return 1; - - switch (msg) { - case WM_ERASEBKGND: - break; - - case WM_SIZE: - InvalidateRect(GraphicsWnd, NULL, false); - break; - - case WM_PAINT: { - // Actually paint the window, with gl. - PaintGraphics(); - // And make Windows happy. - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - EndPaint(hwnd, &ps); - break; - } - - case WM_MOUSELEAVE: - SS.GW.MouseLeave(); - break; - - case WM_MOUSEMOVE: - case WM_LBUTTONDOWN: - case WM_LBUTTONUP: - case WM_LBUTTONDBLCLK: - 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); - - // We need this in order to get the WM_MOUSELEAVE - TRACKMOUSEEVENT tme = {}; - tme.cbSize = sizeof(tme); - tme.dwFlags = TME_LEAVE; - tme.hwndTrack = GraphicsWnd; - TrackMouseEvent(&tme); - - // Convert to xy (vs. ij) style coordinates, with (0, 0) at center - RECT r; - GetClientRect(GraphicsWnd, &r); - x = x - (r.right - r.left)/2; - y = (r.bottom - r.top)/2 - y; - - LastMousePos.x = x; - LastMousePos.y = y; - - if(msg == WM_LBUTTONDOWN) { - SS.GW.MouseLeftDown(x, y); - } else if(msg == WM_LBUTTONUP) { - SS.GW.MouseLeftUp(x, y); - } else if(msg == WM_LBUTTONDBLCLK) { - SS.GW.MouseLeftDoubleClick(x, y); - } else if(msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN) { - SS.GW.MouseMiddleOrRightDown(x, y); - } else if(msg == WM_RBUTTONUP) { - SS.GW.MouseRightUp(x, y); - } else if(msg == WM_MOUSEMOVE) { - SS.GW.MouseMoved(x, y, - !!(wParam & MK_LBUTTON), - !!(wParam & MK_MBUTTON), - !!(wParam & MK_RBUTTON), - !!(wParam & MK_SHIFT), - !!(wParam & MK_CONTROL)); - } - break; - } - case WM_MOUSEWHEEL: - MouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); - break; - - case WM_MENUCOMMAND: { - SolveSpace::Platform::TriggerMenu(GetMenuItemID((HMENU)lParam, wParam)); - break; - } - - case WM_CLOSE: - case WM_DESTROY: - SolveSpaceUI::MenuFile(Command::EXIT); - return 1; - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - - return 1; -} //----------------------------------------------------------------------------- // Common dialog routines, to open or save a file. @@ -1011,16 +324,16 @@ static bool OpenSaveFile(bool isOpen, Platform::Path *filename, const std::strin OPENFILENAME ofn = {}; ofn.lStructSize = sizeof(ofn); - ofn.hInstance = Instance; - ofn.hwndOwner = GraphicsWnd; + ofn.hInstance = NULL; + ofn.hwndOwner = (HWND)SS.GW.window->NativePtr(); ofn.lpstrFilter = selPatternW.c_str(); ofn.lpstrDefExt = defExtensionW.c_str(); ofn.lpstrFile = filenameC; ofn.nMaxFile = len; ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); BOOL r; if(isOpen) { @@ -1029,9 +342,9 @@ static bool OpenSaveFile(bool isOpen, Platform::Path *filename, const std::strin r = GetSaveFileNameW(&ofn); } - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); if(r) *filename = Platform::Path::From(Narrow(filenameC)); return r ? true : false; @@ -1051,18 +364,18 @@ bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExt DialogChoice SolveSpace::SaveFileYesNoCancel() { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - int r = MessageBoxW(GraphicsWnd, + int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), Widen(_("The file has changed since it was last saved.\n\n" "Do you want to save the changes?")).c_str(), Title(C_("title", "Modified File")).c_str(), MB_YESNOCANCEL | MB_ICONWARNING); - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); switch(r) { case IDYES: @@ -1077,18 +390,18 @@ DialogChoice SolveSpace::SaveFileYesNoCancel() DialogChoice SolveSpace::LoadAutosaveYesNo() { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - int r = MessageBoxW(GraphicsWnd, + int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), Widen(_("An autosave file is available for this project.\n\n" "Do you want to load the autosave file instead?")).c_str(), Title(C_("title", "Autosave Available")).c_str(), MB_YESNO | MB_ICONWARNING); - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); switch (r) { case IDYES: @@ -1101,8 +414,8 @@ DialogChoice SolveSpace::LoadAutosaveYesNo() DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const Platform::Path &filename, bool canCancel) { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); std::string message = "The linked file " + filename.raw + " is not present.\n\n" @@ -1110,13 +423,14 @@ DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const Platform::Path &fil "If you select \"No\", any geometry that depends on " "the missing file will be removed."; - int r = MessageBoxW(GraphicsWnd, Widen(message).c_str(), + int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), + Widen(message).c_str(), Title(C_("title", "Missing File")).c_str(), (canCancel ? MB_YESNOCANCEL : MB_YESNO) | MB_ICONWARNING); - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); switch(r) { case IDYES: @@ -1147,88 +461,6 @@ std::vector SolveSpace::GetFontFiles() { return fonts; } -static void CreateMainWindows() -{ - WNDCLASSEX wc = {}; - - wc.cbSize = sizeof(wc); - - // The graphics window, where the sketch is drawn and shown. - wc.style = CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_OWNDC | - CS_DBLCLKS; - wc.lpfnWndProc = (WNDPROC)GraphicsWndProc; - wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); - wc.lpszClassName = L"GraphicsWnd"; - wc.lpszMenuName = NULL; - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hIcon = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), - IMAGE_ICON, 32, 32, 0); - wc.hIconSm = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), - IMAGE_ICON, 16, 16, 0); - ssassert(RegisterClassEx(&wc), "Cannot register window class"); - - GraphicsWnd = CreateWindowExW(0, L"GraphicsWnd", - Title(C_("title", "(new sketch)")).c_str(), - WS_OVERLAPPED | WS_THICKFRAME | WS_CLIPCHILDREN | WS_MAXIMIZEBOX | - WS_MINIMIZEBOX | WS_SYSMENU | WS_SIZEBOX | WS_CLIPSIBLINGS, - 50, 50, 900, 600, NULL, NULL, Instance, NULL); - ssassert(GraphicsWnd != NULL, "Cannot create window"); - - GraphicsEditControl = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", - WS_CHILD | ES_AUTOHSCROLL | WS_TABSTOP | WS_CLIPSIBLINGS, - 50, 50, 100, 21, GraphicsWnd, NULL, Instance, NULL); - - // The text window, with a command line and some textual information - // about the sketch. - wc.style &= ~CS_DBLCLKS; - wc.lpfnWndProc = (WNDPROC)TextWndProc; - wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); - wc.lpszClassName = L"TextWnd"; - wc.hCursor = NULL; - ssassert(RegisterClassEx(&wc), "Cannot register window class"); - - // We get the desired Alt+Tab behaviour by specifying that the text - // window is a child of the graphics window. - TextWnd = CreateWindowExW(0, L"TextWnd", - Title(C_("title", "Property Browser")).c_str(), - WS_THICKFRAME | WS_CLIPCHILDREN, - 650, 500, 420, 300, GraphicsWnd, (HMENU)NULL, Instance, NULL); - ssassert(TextWnd != NULL, "Cannot create window"); - - TextWndScrollBar = CreateWindowExW(0, WC_SCROLLBAR, L"", WS_CHILD | - SBS_VERT | SBS_LEFTALIGN | WS_VISIBLE | WS_CLIPSIBLINGS, - 200, 100, 100, 100, TextWnd, NULL, Instance, NULL); - // Force the scrollbar to get resized to the window, - TextWndProc(TextWnd, WM_SIZE, 0, 0); - - TextEditControl = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", - WS_CHILD | ES_AUTOHSCROLL | WS_TABSTOP | WS_CLIPSIBLINGS, - 50, 50, 100, 21, TextWnd, NULL, Instance, NULL); - -#if HAVE_OPENGL == 3 - // Now that all our windows exist, set up gl contexts. - CreateGlContext(TextWnd, &TextGlDisplay, &TextGlSurface, &TextGlContext); - CreateGlContext(GraphicsWnd, &GraphicsGlDisplay, &GraphicsGlSurface, &GraphicsGlContext); -#else - CreateGlContext(TextWnd, &TextGl); - CreateGlContext(GraphicsWnd, &GraphicsGl); -#endif - - RECT r, rc; - GetWindowRect(TextWnd, &r); - GetClientRect(TextWnd, &rc); - ClientIsSmallerBy = (r.bottom - r.top) - (rc.bottom - rc.top); -} - -void SolveSpace::SetMainMenu(Platform::MenuBarRef menuBar) { - static Platform::MenuBarRef _menuBar; - SetMenu(GraphicsWnd, (HMENU)menuBar->NativePtr()); - _menuBar = menuBar; - - SS.UpdateWindowTitle(); - SetWindowTextW(TextWnd, Title(C_("title", "Property Browser")).c_str()); -} - #ifdef HAVE_SPACEWARE //----------------------------------------------------------------------------- // Test if a message comes from the SpaceNavigator device. If yes, dispatch @@ -1273,9 +505,10 @@ static bool ProcessSpaceNavigatorMsg(MSG *msg) { int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT nCmdShow) { - Instance = hInstance; - - InitCommonControls(); + INITCOMMONCONTROLSEX icc; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_STANDARD_CLASSES|ICC_BAR_CLASSES; + InitCommonControlsEx(&icc); // A monospaced font FixedFont = CreateFontW(SS.TW.CHAR_HEIGHT, SS.TW.CHAR_WIDTH_, 0, 0, @@ -1285,16 +518,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, if(!FixedFont) FixedFont = (HFONT)GetStockObject(SYSTEM_FONT); - // Create the root windows: one for control, with text, and one for - // the graphics - CreateMainWindows(); - - ThawWindowPos(TextWnd, "TextWnd"); - ThawWindowPos(GraphicsWnd, "GraphicsWnd"); - - ShowWindow(TextWnd, SW_SHOWNOACTIVATE); - ShowWindow(GraphicsWnd, SW_SHOW); - std::vector args = InitPlatform(0, NULL); #ifdef HAVE_SPACEWARE @@ -1304,9 +527,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, if(swdc != NULL) { SiOpenData sod; SiInitialize(); - SiOpenWinInit(&sod, GraphicsWnd); - SpaceNavigator = - SiOpen("GraphicsWnd", SI_ANY_DEVICE, SI_NO_MASK, SI_EVENT, &sod); + SiOpenWinInit(&sod, (HWND)SS.GW.window->NativePtr()); + SpaceNavigator = SiOpen("GraphicsWnd", SI_ANY_DEVICE, SI_NO_MASK, SI_EVENT, &sod); SiSetUiMode(SpaceNavigator, SI_UI_NO_CONTROLS); } #endif @@ -1319,16 +541,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, // Call in to the platform-independent code, and let them do their init SS.Init(); - // A filename may have been specified on the command line; if so, then - // strip any quotation marks, and make it absolute. + // A filename may have been specified on the command line. if(args.size() >= 2) { SS.Load(Platform::Path::From(args[1]).Expand(/*fromCurrentDirectory=*/true)); } - // Repaint one more time, after we've set everything up. - PaintGraphics(); - PaintTextWnd(); - // And now it's the message loop. All calls in to the rest of the code // will be from the wndprocs. MSG msg; @@ -1339,17 +556,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, if(ProcessSpaceNavigatorMsg(&msg)) continue; #endif - // A message from the keyboard, which should be processed as a keyboard - // accelerator? - if(msg.message == WM_KEYDOWN) { - if(ProcessKeyDown(msg.wParam)) continue; - } - if(msg.message == WM_SYSKEYDOWN && msg.hwnd == TextWnd) { - // If the user presses the Alt key when the text window has focus, - // then that should probably go to the graphics window instead. - SetForegroundWindow(GraphicsWnd); - } - // None of the above; so just a normal message to process. TranslateMessage(&msg); DispatchMessage(&msg); @@ -1362,10 +568,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, } #endif - // Write everything back to the registry - FreezeWindowPos(TextWnd, "TextWnd"); - FreezeWindowPos(GraphicsWnd, "GraphicsWnd"); - // Free the memory we've used; anything that remains is a leak. SK.Clear(); SS.Clear(); diff --git a/src/render/render.cpp b/src/render/render.cpp index 16a2cc81..3ef7171b 100644 --- a/src/render/render.cpp +++ b/src/render/render.cpp @@ -70,7 +70,7 @@ Vector Camera::VectorFromProjs(Vector rightUpForward) const { } Vector Camera::AlignToPixelGrid(Vector v) const { - if(!hasPixels) return v; + if(!gridFit) return v; v = ProjectPoint3(v); v.x = floor(v.x) + 0.5; diff --git a/src/render/render.h b/src/render/render.h index ac072840..714180d2 100644 --- a/src/render/render.h +++ b/src/render/render.h @@ -17,13 +17,15 @@ enum class StipplePattern : uint32_t; // an axonometric projection. class Camera { public: - size_t width, height; + double width; + double height; + double pixelRatio; + bool gridFit; Vector offset; Vector projRight; Vector projUp; double scale; double tangent; - bool hasPixels; bool IsPerspective() const { return tangent != 0.0; } @@ -255,7 +257,7 @@ public: // A canvas that renders onto a 2d surface, performing z-index sorting, occlusion testing, etc, // on the CPU. -class SurfaceRenderer : public Canvas { +class SurfaceRenderer : public ViewportCanvas { public: Camera camera = {}; Lighting lighting = {}; @@ -273,6 +275,10 @@ public: // Canvas interface. const Camera &GetCamera() const override { return camera; } + // ViewportCanvas interface. + void SetCamera(const Camera &camera) override { this->camera = camera; }; + void SetLighting(const Lighting &lighting) override { this->lighting = lighting; } + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; void DrawEdges(const SEdgeList &el, hStroke hcs) override; bool DrawBeziers(const SBezierList &bl, hStroke hcs) override; @@ -326,6 +332,14 @@ public: hStroke hcs; } current = {}; + void Clear() override; + + void NewFrame() override {} + void FlushFrame() override; + std::shared_ptr ReadFrame() override; + + void GetIdent(const char **vendor, const char **renderer, const char **version) override; + void SelectStroke(hStroke hcs); void MoveTo(Vector p); void FinishPath(); @@ -339,6 +353,18 @@ public: void OutputEnd() override; }; +class CairoPixmapRenderer : public CairoRenderer { +public: + std::shared_ptr pixmap; + + cairo_surface_t *surface = NULL; + + void Init(); + void Clear() override; + + std::shared_ptr ReadFrame() override; +}; + //----------------------------------------------------------------------------- // 3d renderers //----------------------------------------------------------------------------- diff --git a/src/render/rendercairo.cpp b/src/render/rendercairo.cpp index 0aeb946b..3091b792 100644 --- a/src/render/rendercairo.cpp +++ b/src/render/rendercairo.cpp @@ -8,6 +8,30 @@ namespace SolveSpace { +void CairoRenderer::Clear() { + SurfaceRenderer::Clear(); + + if(context != NULL) cairo_destroy(context); + context = NULL; +} + +void CairoRenderer::GetIdent(const char **vendor, const char **renderer, const char **version) { + *vendor = "Cairo"; + *renderer = "Cairo"; + *version = cairo_version_string(); +} + +void CairoRenderer::FlushFrame() { + CullOccludedStrokes(); + OutputInPaintOrder(); + + cairo_surface_flush(cairo_get_target(context)); +} + +std::shared_ptr CairoRenderer::ReadFrame() { + ssassert(false, "generic Cairo renderer does not support pixmap readout"); +} + void CairoRenderer::OutputStart() { cairo_save(context); @@ -31,7 +55,6 @@ void CairoRenderer::OutputEnd() { FinishPath(); cairo_restore(context); - cairo_surface_flush(cairo_get_target(context)); } void CairoRenderer::SelectStroke(hStroke hcs) { @@ -118,4 +141,33 @@ void CairoRenderer::OutputTriangle(const STriangle &tr) { cairo_fill(context); } +void CairoPixmapRenderer::Init() { + Clear(); + + pixmap = std::make_shared(); + pixmap->format = Pixmap::Format::BGRA; + pixmap->width = (size_t)camera.width; + pixmap->height = (size_t)camera.height; + pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, (int)camera.width); + pixmap->data = std::vector(pixmap->stride * pixmap->height); + surface = + cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, + pixmap->width, pixmap->height, + pixmap->stride); + context = cairo_create(surface); +} + +void CairoPixmapRenderer::Clear() { + CairoRenderer::Clear(); + + if(surface != NULL) cairo_surface_destroy(surface); + surface = NULL; +} + +std::shared_ptr CairoPixmapRenderer::ReadFrame() { + std::shared_ptr result = pixmap->Copy(); + result->ConvertTo(Pixmap::Format::RGBA); + return result; +} + } diff --git a/src/render/rendergl1.cpp b/src/render/rendergl1.cpp index edc21347..d06a7391 100644 --- a/src/render/rendergl1.cpp +++ b/src/render/rendergl1.cpp @@ -705,7 +705,9 @@ void OpenGl1Renderer::InvalidatePixmap(std::shared_ptr pm) { void OpenGl1Renderer::UpdateProjection() { UnSelectPrimitive(); - glViewport(0, 0, camera.width, camera.height); + glViewport(0, 0, + camera.width * camera.pixelRatio, + camera.height * camera.pixelRatio); glMatrixMode(GL_PROJECTION); glLoadIdentity(); diff --git a/src/render/rendergl3.cpp b/src/render/rendergl3.cpp index 3256691c..3f409f7d 100644 --- a/src/render/rendergl3.cpp +++ b/src/render/rendergl3.cpp @@ -541,7 +541,9 @@ void OpenGl2Renderer::DrawPixmap(std::shared_ptr pm, } void OpenGl2Renderer::UpdateProjection() { - glViewport(0, 0, camera.width, camera.height); + glViewport(0, 0, + (int)(camera.width * camera.pixelRatio), + (int)(camera.height * camera.pixelRatio)); double mat1[16]; double mat2[16]; @@ -670,7 +672,8 @@ void OpenGl2Renderer::Clear() { std::shared_ptr OpenGl2Renderer::ReadFrame() { std::shared_ptr pixmap = Pixmap::Create(Pixmap::Format::RGB, (size_t)camera.width, (size_t)camera.height); - glReadPixels(0, 0, camera.width, camera.height, GL_RGB, GL_UNSIGNED_BYTE, &pixmap->data[0]); + glReadPixels(0, 0, (int)camera.width, (int)camera.height, + GL_RGB, GL_UNSIGNED_BYTE, &pixmap->data[0]); return pixmap; } diff --git a/src/resource.cpp b/src/resource.cpp index 0f051f88..af0ac3ec 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -354,6 +354,16 @@ std::shared_ptr Pixmap::Create(Format format, size_t width, size_t heigh return pixmap; } +std::shared_ptr Pixmap::Copy() { + std::shared_ptr pixmap = std::make_shared(); + pixmap->format = format; + pixmap->width = width; + pixmap->height = height; + pixmap->stride = stride; + pixmap->data = data; + return pixmap; +} + //----------------------------------------------------------------------------- // ASCII sequence parsing //----------------------------------------------------------------------------- @@ -938,8 +948,8 @@ void VectorFont::Trace(double forCapHeight, Vector o, Vector u, Vector v, const ssassert(!IsEmpty(), "Expected a loaded font"); // Perform grid-fitting only when the text is parallel to the view plane. - if(camera.hasPixels && !(u.WithMagnitude(1).Equals(camera.projRight) && - v.WithMagnitude(1).Equals(camera.projUp))) { + if(camera.gridFit && !(u.WithMagnitude(1).Equals(camera.projRight) && + v.WithMagnitude(1).Equals(camera.projUp))) { return Trace(forCapHeight, o, u, v, str, traceEdge); } diff --git a/src/resource.h b/src/resource.h index 20bf6bd4..6c14dad8 100644 --- a/src/resource.h +++ b/src/resource.h @@ -40,6 +40,8 @@ public: void ConvertTo(Format newFormat); void SetPixel(size_t x, size_t y, RgbaColor color); + + std::shared_ptr Copy(); }; class BitmapFont { diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 3c11d011..e36a32e6 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -127,6 +127,14 @@ void SolveSpaceUI::Init() { NewFile(); AfterNewFile(); + + if(TW.window && GW.window) { + TW.window->ThawPosition("TextWindow"); + TW.window->SetVisible(true); + GW.window->ThawPosition("GraphicsWindow"); + GW.window->SetVisible(true); + GW.window->Focus(); + } } bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) { @@ -161,6 +169,9 @@ bool SolveSpaceUI::Load(const Platform::Path &filename) { } void SolveSpaceUI::Exit() { + GW.window->FreezePosition("GraphicsWindow"); + TW.window->FreezePosition("TextWindow"); + // Recent files for(size_t i = 0; i < MAX_RECENT; i++) { std::string rawPath; @@ -242,7 +253,7 @@ void SolveSpaceUI::Exit() { // And the default styles, colors and line widths and such. Style::FreezeDefaultStyles(); - ExitNow(); + Platform::Exit(); } void SolveSpaceUI::ScheduleGenerateAll() { @@ -339,23 +350,18 @@ void SolveSpaceUI::AfterNewFile() { GenerateAll(Generate::ALL); - TW.Init(); GW.Init(); + TW.Init(); unsaved = false; - int w, h; - GetGraphicsWindowSize(&w, &h); - GW.width = w; - GW.height = h; - - GW.ZoomToFit(/*includingInvisibles=*/false); + GW.ZoomToFit(); // Create all the default styles; they'll get created on the fly anyways, // but can't hurt to do it now. Style::CreateAllDefaultStyles(); - UpdateWindowTitle(); + UpdateWindowTitles(); } void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) { @@ -423,8 +429,18 @@ bool SolveSpaceUI::OkayToStartNewFile() { ssassert(false, "Unexpected dialog choice"); } -void SolveSpaceUI::UpdateWindowTitle() { - SetCurrentFilename(saveFile); +void SolveSpaceUI::UpdateWindowTitles() { + if(!GW.window || !TW.window) return; + + if(saveFile.IsEmpty()) { + GW.window->SetTitle(C_("title", "(new sketch)")); + } else { + if(!GW.window->SetTitleForFilename(saveFile)) { + GW.window->SetTitle(saveFile.raw); + } + } + + TW.window->SetTitle(C_("title", "Property Browser")); } void SolveSpaceUI::MenuFile(Command id) { @@ -553,7 +569,7 @@ void SolveSpaceUI::MenuFile(Command id) { default: ssassert(false, "Unexpected menu ID"); } - SS.UpdateWindowTitle(); + SS.UpdateWindowTitles(); } void SolveSpaceUI::MenuAnalyze(Command id) { @@ -603,7 +619,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { root->MakeCertainEdgesInto(&(SS.nakedEdges), EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks); - InvalidateGraphics(); + SS.GW.Invalidate(); if(inters) { Error("%d edges interfere with other triangles, bad.", @@ -617,7 +633,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { case Command::CENTER_OF_MASS: { SS.UpdateCenterOfMass(); SS.centerOfMass.draw = true; - InvalidateGraphics(); + SS.GW.Invalidate(); break; } @@ -777,7 +793,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { // Clear the trace, and stop tracing SS.traced.point = Entity::NO_ENTITY; SS.traced.path.l.Clear(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; } @@ -798,7 +814,7 @@ void SolveSpaceUI::ShowNakedEdges(bool reportOnlyWhenNotOkay) { if(reportOnlyWhenNotOkay && !inters && !leaks && SS.nakedEdges.l.n == 0) { return; } - InvalidateGraphics(); + SS.GW.Invalidate(); const char *intersMsg = inters ? "The mesh is self-intersecting (NOT okay, invalid)." : @@ -852,6 +868,22 @@ void SolveSpaceUI::Clear() { if(i < undo.cnt) undo.d[i].Clear(); if(i < redo.cnt) redo.d[i].Clear(); } + TW.window = NULL; + GW.openRecentMenu = NULL; + GW.linkRecentMenu = NULL; + GW.showGridMenuItem = NULL; + GW.perspectiveProjMenuItem = NULL; + GW.showToolbarMenuItem = NULL; + GW.showTextWndMenuItem = NULL; + GW.fullScreenMenuItem = NULL; + GW.unitsMmMenuItem = NULL; + GW.unitsMetersMenuItem = NULL; + GW.unitsInchesMenuItem = NULL; + GW.inWorkplaneMenuItem = NULL; + GW.in3dMenuItem = NULL; + GW.undoMenuItem = NULL; + GW.redoMenuItem = NULL; + GW.window = NULL; } void Sketch::Clear() { diff --git a/src/solvespace.h b/src/solvespace.h index a0dcffa7..7eb44332 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -38,6 +38,7 @@ struct FT_LibraryRec_; struct FT_FaceRec_; typedef struct _cairo cairo_t; +typedef struct _cairo_surface cairo_surface_t; // The few floating-point equality comparisons in SolveSpace have been // carefully considered, so we disable the -Wfloat-equal warning for them @@ -164,37 +165,13 @@ std::vector GetFontFiles(); void OpenWebsite(const char *url); -void SetMainMenu(Platform::MenuBarRef menuBar); - -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &str); -void HideGraphicsEditControl(); -bool GraphicsEditControlIsVisible(); -void ShowTextEditControl(int x, int y, const std::string &str); -void HideTextEditControl(); -bool TextEditControlIsVisible(); -void MoveTextScrollbarTo(int pos, int maxPos, int page); - -void ShowTextWindow(bool visible); -void InvalidateText(); -void InvalidateGraphics(); -void PaintGraphics(); -void ToggleFullScreen(); -bool FullScreenIsActive(); -void GetGraphicsWindowSize(int *w, int *h); -void GetTextWindowSize(int *w, int *h); -double GetScreenDpi(); -int64_t GetMilliseconds(); - void dbp(const char *str, ...); #define DBPTRI(tri) \ dbp("tri: (%.3f %.3f %.3f) (%.3f %.3f %.3f) (%.3f %.3f %.3f)", \ CO((tri).a), CO((tri).b), CO((tri).c)) -void SetCurrentFilename(const Platform::Path &filename); void SetMousePointerToHand(bool yes); void DoMessageBox(const char *str, int rows, int cols, bool error); -void ExitNow(); void CnfFreezeInt(uint32_t val, const std::string &name); void CnfFreezeFloat(float val, const std::string &name); @@ -283,6 +260,7 @@ 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); +int64_t GetMilliseconds(); void Message(const char *str, ...); void Error(const char *str, ...); void CnfFreezeBool(bool v, const std::string &name); @@ -730,7 +708,7 @@ public: bool GetFilenameAndSave(bool saveAs); bool OkayToStartNewFile(); hGroup CreateDefaultDrawingGroup(); - void UpdateWindowTitle(); + void UpdateWindowTitles(); void ClearExisting(); void NewFile(); bool SaveToFile(const Platform::Path &filename); diff --git a/src/style.cpp b/src/style.cpp index 9729b3e4..78090c5b 100644 --- a/src/style.cpp +++ b/src/style.cpp @@ -162,7 +162,7 @@ void Style::AssignSelectionToStyle(uint32_t v) { } SS.GW.ClearSelection(); - InvalidateGraphics(); + SS.GW.Invalidate(); // And show that style's info screen in the text window. SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO); @@ -439,7 +439,7 @@ void TextWindow::ScreenDeleteStyle(int link, uint32_t v) { // the style, so no need to do anything else. } SS.TW.GoToScreen(Screen::LIST_OF_STYLES); - InvalidateGraphics(); + SS.GW.Invalidate(); } void TextWindow::ScreenChangeStylePatternType(int link, uint32_t v) { @@ -595,11 +595,10 @@ void TextWindow::ScreenChangeStyleYesNo(int link, uint32_t v) { s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin | (uint32_t)Style::TextOrigin::TOP); break; } - SS.GW.persistentDirty = true; - InvalidateGraphics(); + SS.GW.Invalidate(/*clearPersistent=*/true); } -bool TextWindow::EditControlDoneForStyles(const char *str) { +bool TextWindow::EditControlDoneForStyles(const std::string &str) { Style *s; switch(edit.meaning) { case Edit::STYLE_STIPPLE_PERIOD: @@ -614,7 +613,7 @@ bool TextWindow::EditControlDoneForStyles(const char *str) { if(units == Style::UnitsAs::MM) { v = SS.StringToMm(str); } else { - v = atof(str); + v = atof(str.c_str()); } v = max(0.0, v); if(edit.meaning == Edit::STYLE_TEXT_HEIGHT) { @@ -629,14 +628,14 @@ bool TextWindow::EditControlDoneForStyles(const char *str) { case Edit::STYLE_TEXT_ANGLE: SS.UndoRemember(); s = Style::Get(edit.style); - s->textAngle = WRAP_SYMMETRIC(atof(str), 360); + s->textAngle = WRAP_SYMMETRIC(atof(str.c_str()), 360); break; case Edit::BACKGROUND_COLOR: case Edit::STYLE_FILL_COLOR: case Edit::STYLE_COLOR: { Vector rgb; - if(sscanf(str, "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + if(sscanf(str.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { rgb = rgb.ClampWithin(0, 1); if(edit.meaning == Edit::STYLE_COLOR) { SS.UndoRemember(); @@ -655,7 +654,7 @@ bool TextWindow::EditControlDoneForStyles(const char *str) { break; } case Edit::STYLE_NAME: - if(!*str) { + if(str.empty()) { Error(_("Style name cannot be empty")); } else { SS.UndoRemember(); diff --git a/src/textscreens.cpp b/src/textscreens.cpp index bcd6bf1e..c42a4851 100644 --- a/src/textscreens.cpp +++ b/src/textscreens.cpp @@ -577,10 +577,10 @@ void TextWindow::ScreenStepDimGo(int link, uint32_t v) { // Failed to solve, so quit break; } - PaintGraphics(); + SS.GW.window->Redraw(); } } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.TW.GoToScreen(Screen::LIST_OF_GROUPS); } void TextWindow::ShowStepDimension() { @@ -657,13 +657,12 @@ void TextWindow::ShowTangentArc() { //----------------------------------------------------------------------------- // The edit control is visible, and the user just pressed enter. //----------------------------------------------------------------------------- -void TextWindow::EditControlDone(const char *s) { +void TextWindow::EditControlDone(std::string s) { edit.showAgain = false; switch(edit.meaning) { - case Edit::TIMES_REPEATED: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) { + case Edit::TIMES_REPEATED: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { SS.UndoRemember(); double ev = e->Eval(); @@ -698,9 +697,9 @@ void TextWindow::EditControlDone(const char *s) { SS.MarkGroupDirty(g->h); } break; - } - case Edit::GROUP_NAME: { - if(!*s) { + + case Edit::GROUP_NAME: + if(s.empty()) { Error(_("Group name cannot be empty")); } else { SS.UndoRemember(); @@ -709,10 +708,9 @@ void TextWindow::EditControlDone(const char *s) { g->name = s; } break; - } - case Edit::GROUP_SCALE: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) { + + case Edit::GROUP_SCALE: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { double ev = e->Eval(); if(fabs(ev) < 1e-6) { Error(_("Scale cannot be zero.")); @@ -723,10 +721,10 @@ void TextWindow::EditControlDone(const char *s) { } } break; - } + case Edit::GROUP_COLOR: { Vector rgb; - if(sscanf(s, "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + if(sscanf(s.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { rgb = rgb.ClampWithin(0, 1); Group *g = SK.group.FindByIdNoOops(SS.TW.shown.group); @@ -740,9 +738,8 @@ void TextWindow::EditControlDone(const char *s) { } break; } - case Edit::GROUP_OPACITY: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) { + case Edit::GROUP_OPACITY: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { double alpha = e->Eval(); if(alpha < 0 || alpha > 1) { Error(_("Opacity must be between zero and one.")); @@ -754,42 +751,38 @@ void TextWindow::EditControlDone(const char *s) { } } break; - } - case Edit::TTF_TEXT: { + + case Edit::TTF_TEXT: SS.UndoRemember(); - Request *r = SK.request.FindByIdNoOops(edit.request); - if(r) { + if(Request *r = SK.request.FindByIdNoOops(edit.request)) { r->str = s; SS.MarkGroupDirty(r->group); } break; - } - case Edit::STEP_DIM_FINISH: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(!e) { - break; + + case Edit::STEP_DIM_FINISH: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + if(shown.dimIsDistance) { + shown.dimFinish = SS.ExprToMm(e); + } else { + shown.dimFinish = e->Eval(); + } } - if(shown.dimIsDistance) { - shown.dimFinish = SS.ExprToMm(e); - } else { - shown.dimFinish = e->Eval(); - } - break; - } - case Edit::STEP_DIM_STEPS: - shown.dimSteps = min(300, max(1, atoi(s))); break; - case Edit::TANGENT_ARC_RADIUS: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(!e) break; - if(e->Eval() < LENGTH_EPS) { - Error(_("Radius cannot be zero or negative.")); - break; - } - SS.tangentArcRadius = SS.ExprToMm(e); + case Edit::STEP_DIM_STEPS: + shown.dimSteps = min(300, max(1, atoi(s.c_str()))); + break; + + case Edit::TANGENT_ARC_RADIUS: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + if(e->Eval() < LENGTH_EPS) { + Error(_("Radius cannot be zero or negative.")); + break; + } + SS.tangentArcRadius = SS.ExprToMm(e); + } break; - } default: { int cnt = 0; @@ -801,7 +794,7 @@ void TextWindow::EditControlDone(const char *s) { break; } } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); if(!edit.showAgain) { diff --git a/src/textwin.cpp b/src/textwin.cpp index 8524be3f..9f10caf9 100644 --- a/src/textwin.cpp +++ b/src/textwin.cpp @@ -148,7 +148,7 @@ public: } SS.GenerateAll(); - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); } }; @@ -225,15 +225,49 @@ void TextWindow::MakeColorTable(const Color *in, float *out) { } void TextWindow::Init() { - canvas = CreateRenderer(); + if(!window) { + window = Platform::CreateWindow(Platform::Window::Kind::TOOL, SS.GW.window); + if(window) { + canvas = CreateRenderer(); + + using namespace std::placeholders; + window->onClose = []() { + SS.GW.showTextWindow = false; + SS.GW.EnsureValidActives(); + }; + window->onMouseEvent = [this](Platform::MouseEvent event) { + using Platform::MouseEvent; + + if(event.type == MouseEvent::Type::PRESS || + event.type == MouseEvent::Type::DBL_PRESS || + event.type == MouseEvent::Type::MOTION) { + bool isClick = (event.type != MouseEvent::Type::MOTION); + bool leftDown = (event.button == MouseEvent::Button::LEFT); + this->MouseEvent(isClick, leftDown, event.x, event.y); + return true; + } else if(event.type == MouseEvent::Type::LEAVE) { + MouseLeave(); + return true; + } else if(event.type == MouseEvent::Type::SCROLL_VERT) { + window->SetScrollbarPosition(window->GetScrollbarPosition() - + LINE_HEIGHT / 2 * event.scrollDelta); + } + return false; + }; + window->onKeyboardEvent = SS.GW.window->onKeyboardEvent; + window->onRender = std::bind(&TextWindow::Paint, this); + window->onEditingDone = std::bind(&TextWindow::EditControlDone, this, _1); + window->onScrollbarAdjusted = std::bind(&TextWindow::ScrollbarEvent, this, _1); + window->SetMinContentSize(370, 370); + } + } ClearSuper(); } void TextWindow::ClearSuper() { - HideEditControl(); - // Ugly hack, but not so ugly as the next line + Platform::WindowRef oldWindow = std::move(window); std::shared_ptr oldCanvas = canvas; // Cannot use *this = {} here because TextWindow instances @@ -242,8 +276,11 @@ void TextWindow::ClearSuper() { memset(this, 0, sizeof(*this)); // Return old canvas + window = std::move(oldWindow); canvas = oldCanvas; + HideEditControl(); + MakeColorTable(fgColors, fgColorTable); MakeColorTable(bgColors, bgColorTable); @@ -253,7 +290,10 @@ void TextWindow::ClearSuper() { void TextWindow::HideEditControl() { editControl.colorPicker.show = false; - HideTextEditControl(); + if(window) { + window->HideEditor(); + window->Invalidate(); + } } void TextWindow::ShowEditControl(int col, const std::string &str, int halfRow) { @@ -264,11 +304,13 @@ void TextWindow::ShowEditControl(int col, const std::string &str, int halfRow) { int x = LEFT_MARGIN + CHAR_WIDTH_*col; int y = (halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); - ShowTextEditControl(x, y + 18, str); + double width, height; + window->GetContentSize(&width, &height); + window->ShowEditor(x, y + LINE_HEIGHT - 2, LINE_HEIGHT - 4, + width - x, /*isMonospace=*/true, str); } -void TextWindow::ShowEditControlWithColorPicker(int col, RgbaColor rgb) -{ +void TextWindow::ShowEditControlWithColorPicker(int col, RgbaColor rgb) { SS.ScheduleShowTW(); editControl.colorPicker.show = true; @@ -518,21 +560,35 @@ void TextWindow::Show() { } } - InvalidateText(); + if(window) { + double width, height; + window->GetContentSize(&width, &height); + + halfRows = (int)height / (LINE_HEIGHT/2); + + int bottom = top[rows-1] + 2; + scrollPos = min(scrollPos, bottom - halfRows); + scrollPos = max(scrollPos, 0); + + window->ConfigureScrollbar(0, top[rows - 1] + 1, halfRows); + window->SetScrollbarPosition(scrollPos); + window->SetScrollbarVisible(top[rows - 1] + 1 > halfRows); + window->Invalidate(); + } } void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow how, double mx, double my) { - int width, height; - GetTextWindowSize(&width, &height); + double width, height; + window->GetContentSize(&width, &height); int x = 20, y = 33 + LINE_HEIGHT; y -= scrollPos*(LINE_HEIGHT/2); if(how == PAINT) { int top = y - 28, bot = y + 4; - uiCanvas->DrawRect(0, width, top, bot, + uiCanvas->DrawRect(0, (int)width, top, bot, /*fillColor=*/{ 30, 30, 30, 255 }, /*outlineColor=*/{}); } @@ -546,9 +602,6 @@ void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow button->Draw(uiCanvas, x, y, (button == hoveredButton)); } else if(mx > x - 2 && mx < x + 26 && my < y + 2 && my > y - 26) { - if(button != oldHovered) { - // FIXME(platorm/gui): implement native tooltips here - } hoveredButton = button; if(how == CLICK) { button->Click(); @@ -559,7 +612,12 @@ void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow } if(how != PAINT && hoveredButton != oldHovered) { - InvalidateText(); + if(hoveredButton == NULL) { + window->SetTooltip(""); + } else { + window->SetTooltip(hoveredButton->Tooltip()); + } + window->Invalidate(); } } @@ -629,12 +687,14 @@ std::shared_ptr TextWindow::HsvPattern1d(double hue, double sat, int w, void TextWindow::ColorPickerDone() { RgbaColor rgb = editControl.colorPicker.rgb; - EditControlDone(ssprintf("%.2f, %.2f, %.3f", rgb.redF(), rgb.greenF(), rgb.blueF()).c_str()); + EditControlDone(ssprintf("%.2f, %.2f, %.3f", rgb.redF(), rgb.greenF(), rgb.blueF())); } bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, bool leftDown, double x, double y) { + using Platform::Window; + bool mousePointerAsHand = false; if(how == HOVER && !leftDown) { @@ -643,7 +703,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, } if(!editControl.colorPicker.show) return false; - if(how == CLICK || (how == HOVER && leftDown)) InvalidateText(); + if(how == CLICK || (how == HOVER && leftDown)) window->Invalidate(); static const RgbaColor BaseColor[12] = { RGBi(255, 0, 0), @@ -662,8 +722,8 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, RGBi( 0, 127, 255), }; - int width, height; - GetTextWindowSize(&width, &height); + double width, height; + window->GetContentSize(&width, &height); int px = LEFT_MARGIN + CHAR_WIDTH_*editControl.col; int py = (editControl.halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); @@ -673,7 +733,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, static const int WIDTH = 16, HEIGHT = 12; static const int PITCH = 18, SIZE = 15; - px = min(px, width - (WIDTH*PITCH + 40)); + px = min(px, (int)width - (WIDTH*PITCH + 40)); int pxm = px + WIDTH*PITCH + 11, pym = py + HEIGHT*PITCH + 7; @@ -818,22 +878,26 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, } } - SetMousePointerToHand(mousePointerAsHand); + window->SetCursor(mousePointerAsHand ? + Window::Cursor::HAND : + Window::Cursor::POINTER); return true; } void TextWindow::Paint() { if (!canvas) return; - int width, height; - GetTextWindowSize(&width, &height); + double width, height; + window->GetContentSize(&width, &height); Camera camera = {}; - camera.width = width; - camera.height = height; + camera.width = width; + camera.height = height; + camera.pixelRatio = window->GetDevicePixelRatio(); + camera.gridFit = (window->GetDevicePixelRatio() == 1); camera.LoadIdentity(); - camera.offset.x = -(double)camera.width / 2.0; - camera.offset.y = -(double)camera.height / 2.0; + camera.offset.x = -camera.width / 2.0; + camera.offset.y = -camera.height / 2.0; Lighting lighting = {}; lighting.backgroundColor = RGBi(0, 0, 0); @@ -846,16 +910,6 @@ void TextWindow::Paint() { uiCanvas.canvas = canvas; uiCanvas.flip = true; - halfRows = camera.height / (LINE_HEIGHT/2); - - int bottom = top[rows-1] + 2; - scrollPos = min(scrollPos, bottom - halfRows); - scrollPos = max(scrollPos, 0); - - // Let's set up the scroll bar first - MoveTextScrollbarTo(scrollPos, top[rows - 1] + 1, halfRows); - - // Now paint the window. int r, c, a; for(a = 0; a < 2; a++) { for(r = 0; r < rows; r++) { @@ -863,7 +917,7 @@ void TextWindow::Paint() { if(ltop < (scrollPos-1)) continue; if(ltop > scrollPos+halfRows) break; - for(c = 0; c < min((width/CHAR_WIDTH_)+1, (int) MAX_COLS); c++) { + for(c = 0; c < min(((int)width/CHAR_WIDTH_)+1, (int) MAX_COLS); c++) { int x = LEFT_MARGIN + c*CHAR_WIDTH_; int y = (ltop-scrollPos)*(LINE_HEIGHT/2) + 4; @@ -971,17 +1025,18 @@ void TextWindow::Paint() { } void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { - if(TextEditControlIsVisible() || GraphicsEditControlIsVisible()) { - if(DrawOrHitTestColorPicker(NULL, leftClick ? CLICK : HOVER, leftDown, x, y)) - { + using Platform::Window; + + if(SS.TW.window->IsEditorVisible() || SS.GW.window->IsEditorVisible()) { + if(DrawOrHitTestColorPicker(NULL, leftClick ? CLICK : HOVER, leftDown, x, y)) { return; } if(leftClick) { HideEditControl(); - HideGraphicsEditControl(); + SS.GW.window->HideEditor(); } else { - SetMousePointerToHand(false); + window->SetCursor(Window::Cursor::POINTER); } return; } @@ -1007,7 +1062,7 @@ void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { } } if(r >= 0 && c >= 0 && r < rows && c < MAX_COLS) { - SetMousePointerToHand(false); + window->SetCursor(Window::Cursor::POINTER); hoveredRow = r; hoveredCol = c; @@ -1017,16 +1072,16 @@ void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { if(item.link && item.f) { (item.f)(item.link, item.data); Show(); - InvalidateGraphics(); + SS.GW.Invalidate(); } } else { if(item.link) { - SetMousePointerToHand(true); + window->SetCursor(Window::Cursor::HAND); if(item.h) { (item.h)(item.link, item.data); } } else { - SetMousePointerToHand(false); + window->SetCursor(Window::Cursor::POINTER); } } } @@ -1035,8 +1090,8 @@ void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { prevHoveredRow != hoveredRow || prevHoveredCol != hoveredCol) { - InvalidateGraphics(); - InvalidateText(); + SS.GW.Invalidate(); + window->Invalidate(); } } @@ -1044,21 +1099,20 @@ void TextWindow::MouseLeave() { hoveredButton = NULL; hoveredRow = 0; hoveredCol = 0; - InvalidateText(); + window->Invalidate(); } -void TextWindow::ScrollbarEvent(int newPos) { - if(TextEditControlIsVisible()) +void TextWindow::ScrollbarEvent(double newPos) { + if(window->IsEditorVisible()) return; int bottom = top[rows-1] + 2; - newPos = min(newPos, bottom - halfRows); - newPos = max(newPos, 0); + newPos = min((int)newPos, bottom - halfRows); + newPos = max((int)newPos, 0); if(newPos != scrollPos) { - scrollPos = newPos; - MoveTextScrollbarTo(scrollPos, top[rows - 1] + 1, halfRows); - InvalidateText(); + scrollPos = (int)newPos; + window->Invalidate(); } } diff --git a/src/toolbar.cpp b/src/toolbar.cpp index fafc39cb..756cba07 100644 --- a/src/toolbar.cpp +++ b/src/toolbar.cpp @@ -91,34 +91,61 @@ void GraphicsWindow::ToolbarDraw(UiCanvas *canvas) { } bool GraphicsWindow::ToolbarMouseMoved(int x, int y) { - x += ((int)width/2); - y += ((int)height/2); + double width, height; + window->GetContentSize(&width, &height); - Command nh = Command::NONE; - bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &nh); - if(!withinToolbar) nh = Command::NONE; - - if(nh != toolbarHovered) { - toolbarHovered = nh; - // FIXME(platorm/gui): implement native tooltips here - PaintGraphics(); - } - return withinToolbar; -} - -bool GraphicsWindow::ToolbarMouseDown(int x, int y) { x += ((int)width/2); y += ((int)height/2); Command hit; bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hit); - SS.GW.ActivateCommand(hit); + + if(hit != toolbarHovered) { + toolbarHovered = hit; + Invalidate(); + } + + if(toolbarHovered != Command::NONE) { + std::string tooltip; + for(ToolIcon &icon : Toolbar) { + if(toolbarHovered == icon.command) { + tooltip = Translate(icon.tooltip); + } + } + + Platform::KeyboardEvent accel = SS.GW.AcceleratorForCommand(toolbarHovered); + std::string accelDesc = Platform::AcceleratorDescription(accel); + if(!accelDesc.empty()) { + tooltip += ssprintf(" (%s)", accelDesc.c_str()); + } + + window->SetTooltip(tooltip); + } + + return withinToolbar; +} + +bool GraphicsWindow::ToolbarMouseDown(int x, int y) { + double width, height; + window->GetContentSize(&width, &height); + + x += ((int)width/2); + y += ((int)height/2); + + Command hit; + bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hit); + if(hit != Command::NONE) { + SS.GW.ActivateCommand(hit); + } return withinToolbar; } bool GraphicsWindow::ToolbarDrawOrHitTest(int mx, int my, UiCanvas *canvas, Command *menuHit) { + double width, height; + window->GetContentSize(&width, &height); + int x = 17, y = (int)(height - 52); int fudge = 8; diff --git a/src/ui.h b/src/ui.h index 339f6cd9..f5c002c8 100644 --- a/src/ui.h +++ b/src/ui.h @@ -276,16 +276,15 @@ public: int top[MAX_ROWS]; // in half-line units, or -1 for unused int rows; + Platform::WindowRef window; std::shared_ptr canvas; void Draw(Canvas *canvas); - // These are called by the platform-specific code. void Paint(); void MouseEvent(bool isClick, bool leftDown, double x, double y); - void MouseScroll(double x, double y, int delta); void MouseLeave(); - void ScrollbarEvent(int newPos); + void ScrollbarEvent(double newPos); enum DrawOrHitHow : uint32_t { PAINT = 0, @@ -532,18 +531,19 @@ public: static void ScreenChangeViewOrigin(int link, uint32_t v); static void ScreenChangeViewProjection(int link, uint32_t v); - bool EditControlDoneForStyles(const char *s); - bool EditControlDoneForConfiguration(const char *s); - bool EditControlDoneForPaste(const char *s); - bool EditControlDoneForView(const char *s); - void EditControlDone(const char *s); + bool EditControlDoneForStyles(const std::string &s); + bool EditControlDoneForConfiguration(const std::string &s); + bool EditControlDoneForPaste(const std::string &s); + bool EditControlDoneForView(const std::string &s); + void EditControlDone(std::string s); }; class GraphicsWindow { public: void Init(); - Platform::MenuBarRef mainMenu; + Platform::WindowRef window; + void PopulateMainMenu(); void PopulateRecentFiles(); @@ -581,8 +581,6 @@ public: std::shared_ptr persistentCanvas; bool persistentDirty; - // The width and height (in pixels) of the window. - double width, height; // These parameters define the map from 2d screen coordinates to the // coordinates of the 3d sketch points. We will use an axonometric // projection. @@ -635,13 +633,17 @@ public: void AnimateOntoWorkplane(); Vector VectorFromProjs(Vector rightUpForward); void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, - double *wmin, bool usePerspective); + double *wmin, bool usePerspective, + const Camera &camera); void LoopOverPoints(const std::vector &entities, const std::vector &constraints, const std::vector &faces, Point2d *pmax, Point2d *pmin, - double *wmin, bool usePerspective, bool includeMesh); - void ZoomToFit(bool includingInvisibles, bool useSelection = false); + double *wmin, bool usePerspective, bool includeMesh, + const Camera &camera); + void ZoomToFit(bool includingInvisibles = false, bool useSelection = false); + double ZoomToFit(const Camera &camera, + bool includingInvisibles = false, bool useSelection = false); hGroup activeGroup; void EnsureValidActives(); @@ -831,14 +833,15 @@ public: void UpdateDraggedNum(Vector *pos, double mx, double my); void UpdateDraggedPoint(hEntity hp, double mx, double my); + void Invalidate(bool clearPersistent = false); void DrawEntities(Canvas *canvas, bool persistent); void DrawPersistent(Canvas *canvas); void Draw(Canvas *canvas); - - // These are called by the platform-specific code. void Paint(); + + bool MouseEvent(Platform::MouseEvent event); void MouseMoved(double x, double y, bool leftDown, bool middleDown, - bool rightDown, bool shiftDown, bool ctrlDown); + bool rightDown, bool shiftDown, bool ctrlDown); void MouseLeftDown(double x, double y); void MouseLeftUp(double x, double y); void MouseLeftDoubleClick(double x, double y); @@ -847,7 +850,7 @@ public: void MouseScroll(double x, double y, int delta); void MouseLeave(); bool KeyboardEvent(Platform::KeyboardEvent event); - void EditControlDone(const char *s); + void EditControlDone(const std::string &s); int64_t lastSpaceNavigatorTime; hGroup lastSpaceNavigatorGroup; diff --git a/src/view.cpp b/src/view.cpp index a51f080c..14d09ba3 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -45,7 +45,7 @@ void TextWindow::ScreenChangeViewScale(int link, uint32_t v) { } void TextWindow::ScreenChangeViewToFullScale(int link, uint32_t v) { - SS.GW.scale = GetScreenDpi() / 25.4; + SS.GW.scale = SS.GW.window->GetPixelDensity() / 25.4; } void TextWindow::ScreenChangeViewOrigin(int link, uint32_t v) { @@ -66,7 +66,7 @@ void TextWindow::ScreenChangeViewProjection(int link, uint32_t v) { SS.TW.ShowEditControl(10, edit_value); } -bool TextWindow::EditControlDoneForView(const char *s) { +bool TextWindow::EditControlDoneForView(const std::string &s) { switch(edit.meaning) { case Edit::VIEW_SCALE: { Expr *e = Expr::From(s, /*popUpError=*/true); @@ -83,7 +83,7 @@ bool TextWindow::EditControlDoneForView(const char *s) { case Edit::VIEW_ORIGIN: { Vector pt; - if(sscanf(s, "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) == 3) { + if(sscanf(s.c_str(), "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) == 3) { pt = pt.ScaledBy(SS.MmPerUnit()); SS.GW.offset = pt.ScaledBy(-1); } else { @@ -95,7 +95,7 @@ bool TextWindow::EditControlDoneForView(const char *s) { case Edit::VIEW_PROJ_RIGHT: case Edit::VIEW_PROJ_UP: { Vector pt; - if(sscanf(s, "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) != 3) { + if(sscanf(s.c_str(), "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) != 3) { Error(_("Bad format: specify x, y, z")); break; } diff --git a/test/constraint/arc_line_tangent/normal.png b/test/constraint/arc_line_tangent/normal.png index 67fc0e3f..b885995f 100644 Binary files a/test/constraint/arc_line_tangent/normal.png and b/test/constraint/arc_line_tangent/normal.png differ diff --git a/test/constraint/curve_curve_tangent/arc_arc.png b/test/constraint/curve_curve_tangent/arc_arc.png index 2ee4ea9d..c6ceb2ab 100644 Binary files a/test/constraint/curve_curve_tangent/arc_arc.png and b/test/constraint/curve_curve_tangent/arc_arc.png differ diff --git a/test/constraint/curve_curve_tangent/arc_cubic.png b/test/constraint/curve_curve_tangent/arc_cubic.png index 90c4c6ad..647e4fe0 100644 Binary files a/test/constraint/curve_curve_tangent/arc_cubic.png and b/test/constraint/curve_curve_tangent/arc_cubic.png differ diff --git a/test/constraint/diameter/normal.png b/test/constraint/diameter/normal.png index a53fdaf8..c58025bd 100644 Binary files a/test/constraint/diameter/normal.png and b/test/constraint/diameter/normal.png differ diff --git a/test/constraint/diameter/reference.png b/test/constraint/diameter/reference.png index 42cd96f1..b923aeab 100644 Binary files a/test/constraint/diameter/reference.png and b/test/constraint/diameter/reference.png differ diff --git a/test/constraint/equal_line_arc_len/normal.png b/test/constraint/equal_line_arc_len/normal.png index 3031ca66..bb0e2181 100644 Binary files a/test/constraint/equal_line_arc_len/normal.png and b/test/constraint/equal_line_arc_len/normal.png differ diff --git a/test/constraint/equal_line_arc_len/pi.png b/test/constraint/equal_line_arc_len/pi.png index e7bccc7a..9207fcaa 100644 Binary files a/test/constraint/equal_line_arc_len/pi.png and b/test/constraint/equal_line_arc_len/pi.png differ diff --git a/test/constraint/equal_line_arc_len/tau.png b/test/constraint/equal_line_arc_len/tau.png index 763e235c..5336ac96 100644 Binary files a/test/constraint/equal_line_arc_len/tau.png and b/test/constraint/equal_line_arc_len/tau.png differ diff --git a/test/constraint/equal_radius/normal.png b/test/constraint/equal_radius/normal.png index 22144dfa..c7f8e797 100644 Binary files a/test/constraint/equal_radius/normal.png and b/test/constraint/equal_radius/normal.png differ diff --git a/test/constraint/pt_on_circle/normal.png b/test/constraint/pt_on_circle/normal.png index ef105be8..fe0734c2 100644 Binary files a/test/constraint/pt_on_circle/normal.png and b/test/constraint/pt_on_circle/normal.png differ diff --git a/test/constraint/pt_on_face/normal.png b/test/constraint/pt_on_face/normal.png index 62080a07..1f7a7e89 100644 Binary files a/test/constraint/pt_on_face/normal.png and b/test/constraint/pt_on_face/normal.png differ diff --git a/test/group/translate_asy/test.cpp b/test/group/translate_asy/test.cpp index f1d22278..957dccef 100644 --- a/test/group/translate_asy/test.cpp +++ b/test/group/translate_asy/test.cpp @@ -15,6 +15,7 @@ TEST_CASE(normal_inters) { CHECK_LOAD("normal.slvs"); Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); SMesh *m = &g->displayMesh; SEdgeList el = {}; diff --git a/test/harness.cpp b/test/harness.cpp index 70f340c6..09635dee 100644 --- a/test/harness.cpp +++ b/test/harness.cpp @@ -17,8 +17,6 @@ namespace SolveSpace { // These are defined in headless.cpp, and aren't exposed in solvespace.h. extern std::vector fontFiles; - extern bool antialias; - extern std::shared_ptr framebuffer; } // The paths in __FILE__ are from the build system, but defined(WIN32) returns @@ -237,24 +235,45 @@ bool Test::Helper::CheckSave(const char *file, int line, const char *reference) } bool Test::Helper::CheckRender(const char *file, int line, const char *reference) { - PaintGraphics(); + // First, render to a framebuffer. + Camera camera = {}; + camera.pixelRatio = 1; + camera.gridFit = true; + camera.width = 600; + camera.height = 600; + camera.projUp = SS.GW.projUp; + camera.projRight = SS.GW.projRight; + camera.scale = SS.GW.scale; + CairoPixmapRenderer pixmapCanvas; + pixmapCanvas.SetLighting(SS.GW.GetLighting()); + pixmapCanvas.SetCamera(camera); + pixmapCanvas.Init(); + + pixmapCanvas.NewFrame(); + SS.GW.Draw(&pixmapCanvas); + pixmapCanvas.FlushFrame(); + std::shared_ptr frame = pixmapCanvas.ReadFrame(); + + pixmapCanvas.Clear(); + + // Now, diff framebuffer against reference render. Platform::Path refPath = GetAssetPath(file, reference), outPath = GetAssetPath(file, reference, "out"), diffPath = GetAssetPath(file, reference, "diff"); std::shared_ptr refPixmap = Pixmap::ReadPng(refPath, /*flip=*/true); - if(!RecordCheck(refPixmap && refPixmap->Equals(*framebuffer))) { - framebuffer->WritePng(outPath, /*flip=*/true); + if(!RecordCheck(refPixmap && refPixmap->Equals(*frame))) { + frame->WritePng(outPath, /*flip=*/true); if(!refPixmap) { PrintFailure(file, line, "reference render not present"); return false; } - ssassert(refPixmap->format == framebuffer->format, "Expected buffer formats to match"); - if(refPixmap->width != framebuffer->width || - refPixmap->height != framebuffer->height) { + ssassert(refPixmap->format == frame->format, "Expected buffer formats to match"); + if(refPixmap->width != frame->width || + refPixmap->height != frame->height) { PrintFailure(file, line, "render doesn't match reference; dimensions differ"); } else { std::shared_ptr diffPixmap = @@ -263,7 +282,7 @@ bool Test::Helper::CheckRender(const char *file, int line, const char *reference int diffPixelCount = 0; for(size_t j = 0; j < refPixmap->height; j++) { for(size_t i = 0; i < refPixmap->width; i++) { - if(!refPixmap->GetPixel(i, j).Equals(framebuffer->GetPixel(i, j))) { + if(!refPixmap->GetPixel(i, j).Equals(frame->GetPixel(i, j))) { diffPixelCount++; diffPixmap->SetPixel(i, j, RgbaColor::From(255, 0, 0, 255)); } @@ -321,9 +340,6 @@ int main(int argc, char **argv) { fontFiles.push_back(HostRoot().Join("Gentium-R.ttf")); - // Different Cairo versions have different antialiasing algorithms. - antialias = false; - // Wreck order dependencies between tests! std::random_shuffle(testCasesPtr->begin(), testCasesPtr->end()); diff --git a/test/request/arc_of_circle/normal.png b/test/request/arc_of_circle/normal.png index 766cb15e..e006a1d8 100644 Binary files a/test/request/arc_of_circle/normal.png and b/test/request/arc_of_circle/normal.png differ diff --git a/test/request/circle/free_in_3d.png b/test/request/circle/free_in_3d.png index c735d860..86257e07 100644 Binary files a/test/request/circle/free_in_3d.png and b/test/request/circle/free_in_3d.png differ diff --git a/test/request/circle/free_in_3d_dof.png b/test/request/circle/free_in_3d_dof.png index 010296d2..0a4422b0 100644 Binary files a/test/request/circle/free_in_3d_dof.png and b/test/request/circle/free_in_3d_dof.png differ diff --git a/test/request/circle/normal.png b/test/request/circle/normal.png index c735d860..86257e07 100644 Binary files a/test/request/circle/normal.png and b/test/request/circle/normal.png differ diff --git a/test/request/circle/normal_dof.png b/test/request/circle/normal_dof.png index baf11cfb..a032f1aa 100644 Binary files a/test/request/circle/normal_dof.png and b/test/request/circle/normal_dof.png differ diff --git a/test/request/cubic/normal.png b/test/request/cubic/normal.png index 20d23502..c4fc0504 100644 Binary files a/test/request/cubic/normal.png and b/test/request/cubic/normal.png differ diff --git a/test/request/cubic_periodic/normal.png b/test/request/cubic_periodic/normal.png index c113d921..7b935c82 100644 Binary files a/test/request/cubic_periodic/normal.png and b/test/request/cubic_periodic/normal.png differ diff --git a/test/request/ttf_text/normal.png b/test/request/ttf_text/normal.png index 39f789ef..9aa4d185 100644 Binary files a/test/request/ttf_text/normal.png and b/test/request/ttf_text/normal.png differ