Implement a platform abstraction for windows.
This commit removes a large amount of code partially duplicated between the text and the graphics windows, and opens the path to having more than one model window on screen at any given time, as well as simplifies platform work. This commit also adds complete support for High-DPI device pixel ratio. It adds support for font scale factor (a fractional factor on top of integral device pixel ratio) on the platform side, but not on the application side. This commit also adds error checking to all Windows API calls (within the abstracted code) and fixes a significant number of misuses and non-future-proof uses of Windows API. This commit also makes uses of Windows API idiomatic, e.g. using the built-in vertical scroll bar, native tooltips, control subclassing instead of hooks in the global dispatch loop, and so on. It reinstates tooltip support and removes menu-related hacks.pull/307/merge
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="JonathanWesthues.3dCAD.SolveSpace"
|
||||
type="win32"
|
||||
/>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<description>Parametric 3d CAD tool.</description>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
<dpiAware>true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
|
|
|
@ -329,7 +329,7 @@ if(ENABLE_GUI)
|
|||
|
||||
if(MSVC)
|
||||
set_target_properties(solvespace PROPERTIES
|
||||
LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF")
|
||||
LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF /STACK:33554432")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
@ -325,7 +325,7 @@ void GraphicsWindow::MenuClipboard(Command id) {
|
|||
}
|
||||
}
|
||||
|
||||
bool TextWindow::EditControlDoneForPaste(const char *s) {
|
||||
bool TextWindow::EditControlDoneForPaste(const std::string &s) {
|
||||
Expr *e;
|
||||
switch(edit.meaning) {
|
||||
case Edit::PASTE_TIMES_REPEATED: {
|
||||
|
|
|
@ -80,28 +80,27 @@ void TextWindow::ScreenChangeFixExportColors(int link, uint32_t v) {
|
|||
|
||||
void TextWindow::ScreenChangeBackFaces(int link, uint32_t v) {
|
||||
SS.drawBackFaces = !SS.drawBackFaces;
|
||||
SS.GW.persistentDirty = true;
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate(/*clearPersistent=*/true);
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangeShowContourAreas(int link, uint32_t v) {
|
||||
SS.showContourAreas = !SS.showContourAreas;
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangeCheckClosedContour(int link, uint32_t v) {
|
||||
SS.checkClosedContour = !SS.checkClosedContour;
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangeShadedTriangles(int link, uint32_t v) {
|
||||
SS.exportShadedTriangles = !SS.exportShadedTriangles;
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangePwlCurves(int link, uint32_t v) {
|
||||
SS.exportPwlCurves = !SS.exportPwlCurves;
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangeCanvasSizeAuto(int link, uint32_t v) {
|
||||
|
@ -110,7 +109,7 @@ void TextWindow::ScreenChangeCanvasSizeAuto(int link, uint32_t v) {
|
|||
} else {
|
||||
SS.exportCanvasSizeAuto = false;
|
||||
}
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangeCanvasSize(int link, uint32_t v) {
|
||||
|
@ -321,26 +320,26 @@ void TextWindow::ShowConfiguration() {
|
|||
}
|
||||
}
|
||||
|
||||
bool TextWindow::EditControlDoneForConfiguration(const char *s) {
|
||||
bool TextWindow::EditControlDoneForConfiguration(const std::string &s) {
|
||||
switch(edit.meaning) {
|
||||
case Edit::LIGHT_INTENSITY:
|
||||
SS.lightIntensity[edit.i] = min(1.0, max(0.0, atof(s)));
|
||||
InvalidateGraphics();
|
||||
SS.lightIntensity[edit.i] = min(1.0, max(0.0, atof(s.c_str())));
|
||||
SS.GW.Invalidate();
|
||||
break;
|
||||
|
||||
case Edit::LIGHT_DIRECTION: {
|
||||
double x, y, z;
|
||||
if(sscanf(s, "%lf, %lf, %lf", &x, &y, &z)==3) {
|
||||
if(sscanf(s.c_str(), "%lf, %lf, %lf", &x, &y, &z)==3) {
|
||||
SS.lightDir[edit.i] = Vector::From(x, y, z);
|
||||
} else {
|
||||
Error(_("Bad format: specify coordinates as x, y, z"));
|
||||
}
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
break;
|
||||
}
|
||||
case Edit::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);
|
||||
SS.modelColor[edit.i] = RGBf(rgb.x, rgb.y, rgb.z);
|
||||
} else {
|
||||
|
@ -350,44 +349,44 @@ bool TextWindow::EditControlDoneForConfiguration(const char *s) {
|
|||
}
|
||||
case Edit::CHORD_TOLERANCE: {
|
||||
if(edit.i == 0) {
|
||||
SS.chordTol = max(0.0, atof(s));
|
||||
SS.chordTol = max(0.0, atof(s.c_str()));
|
||||
SS.GenerateAll(SolveSpaceUI::Generate::ALL);
|
||||
} else {
|
||||
SS.exportChordTol = max(0.0, atof(s));
|
||||
SS.exportChordTol = max(0.0, atof(s.c_str()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Edit::MAX_SEGMENTS: {
|
||||
if(edit.i == 0) {
|
||||
SS.maxSegments = min(1000, max(7, atoi(s)));
|
||||
SS.maxSegments = min(1000, max(7, atoi(s.c_str())));
|
||||
SS.GenerateAll(SolveSpaceUI::Generate::ALL);
|
||||
} else {
|
||||
SS.exportMaxSegments = min(1000, max(7, atoi(s)));
|
||||
SS.exportMaxSegments = min(1000, max(7, atoi(s.c_str())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Edit::CAMERA_TANGENT: {
|
||||
SS.cameraTangent = (min(2.0, max(0.0, atof(s))))/1000.0;
|
||||
SS.cameraTangent = (min(2.0, max(0.0, atof(s.c_str()))))/1000.0;
|
||||
if(!SS.usePerspectiveProj) {
|
||||
Message(_("The perspective factor will have no effect until you "
|
||||
"enable View -> 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();
|
||||
|
|
|
@ -744,7 +744,7 @@ void Constraint::MenuConstrain(Command id) {
|
|||
}
|
||||
|
||||
SS.GW.ClearSelection();
|
||||
InvalidateGraphics();
|
||||
SS.GW.Invalidate();
|
||||
}
|
||||
|
||||
#endif /* ! LIBRARY */
|
||||
|
|
33
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;
|
||||
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();
|
||||
camera.hasPixels = true;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
26
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<Token> 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;
|
||||
|
|
|
@ -96,8 +96,8 @@ public:
|
|||
Expr *DeepCopyWithParamsAsPointers(IdList<Param,hParam> *firstTry,
|
||||
IdList<Param,hParam> *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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,14 +362,6 @@ void GraphicsWindow::PopulateRecentFiles() {
|
|||
}
|
||||
|
||||
void GraphicsWindow::Init() {
|
||||
PopulateMainMenu();
|
||||
|
||||
canvas = CreateRenderer();
|
||||
if(canvas) {
|
||||
persistentCanvas = canvas->CreateBatch();
|
||||
persistentDirty = true;
|
||||
}
|
||||
|
||||
scale = 5;
|
||||
offset = Vector::From(0, 0, 0);
|
||||
projRight = Vector::From(1, 0, 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<Entity *> &entities,
|
|||
const std::vector<Constraint *> &constraints,
|
||||
const std::vector<hEntity> &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<Entity *> &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<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<Entity *> &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<Entity *> entities;
|
||||
std::vector<Constraint *> constraints;
|
||||
std::vector<hEntity> 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();
|
||||
}
|
||||
|
||||
|
|
119
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -191,16 +191,32 @@ static bool RunCommand(const std::vector<std::string> 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++) {
|
||||
|
|
|
@ -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<NSWindowDelegate>
|
||||
- (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<NSWindowDelegate>
|
||||
- (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::Platform::Path> SolveSpace::GetFontFiles() {
|
|||
/* Application lifecycle */
|
||||
|
||||
@interface ApplicationDelegate : NSObject<NSApplicationDelegate>
|
||||
- (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::Platform::Path> 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();
|
||||
|
|
|
@ -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<Pango::Layout> 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<void, Glib::ustring> 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<void, Glib::ustring> _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<Gdk::GLContext> 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<Gdk::GLContext> &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<GraphicsWindowGtk> 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<Gtk::Adjustment> 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<Gdk::Window> 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<Gdk::GLContext> on_create_context() override {
|
||||
return get_window()->create_gl_context();
|
||||
}
|
||||
|
||||
bool on_render(const Glib::RefPtr<Gdk::GLContext> &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<Gtk::Adjustment> _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<TextWindowGtk> 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();
|
||||
|
|
|
@ -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<Menu> AddSubMenu(const std::string &label) = 0;
|
||||
|
||||
virtual void Clear() = 0;
|
||||
virtual void *NativePtr() = 0;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<MenuBar> MenuBarRef;
|
||||
|
@ -114,6 +143,87 @@ typedef std::shared_ptr<MenuBar> 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<void()> onClose;
|
||||
std::function<void(bool)> onFullScreen;
|
||||
std::function<bool(MouseEvent)> onMouseEvent;
|
||||
std::function<bool(KeyboardEvent)> onKeyboardEvent;
|
||||
std::function<void(std::string)> onEditingDone;
|
||||
std::function<void(double)> onScrollbarAdjusted;
|
||||
std::function<void()> 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<Window> WindowRef;
|
||||
|
||||
WindowRef CreateWindow(Window::Kind kind = Window::Kind::TOPLEVEL,
|
||||
WindowRef parentWindow = NULL);
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Application-wide APIs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void Exit();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -3,12 +3,19 @@
|
|||
//
|
||||
// Copyright 2018 whitequark
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "solvespace.h"
|
||||
#include <glibmm/main.h>
|
||||
#include <gtkmm/box.h>
|
||||
#include <gtkmm/checkmenuitem.h>
|
||||
#include <gtkmm/separatormenuitem.h>
|
||||
#include <gtkmm/entry.h>
|
||||
#include <gtkmm/fixed.h>
|
||||
#include <gtkmm/glarea.h>
|
||||
#include <gtkmm/main.h>
|
||||
#include <gtkmm/menu.h>
|
||||
#include <gtkmm/menubar.h>
|
||||
#include "solvespace.h"
|
||||
#include <gtkmm/scrollbar.h>
|
||||
#include <gtkmm/separatormenuitem.h>
|
||||
#include <gtkmm/window.h>
|
||||
|
||||
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<MenuBarImplGtk>();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// 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<Gdk::GLContext> on_create_context() override {
|
||||
return get_window()->create_gl_context();
|
||||
}
|
||||
|
||||
bool on_render(const Glib::RefPtr<Gdk::GLContext> &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<Pango::Layout> 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<WindowImplGtk>(kind);
|
||||
if(parentWindow) {
|
||||
window->gtkWindow.set_transient_for(
|
||||
std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow);
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Application-wide APIs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void Exit() {
|
||||
Gtk::Main::quit();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,19 @@
|
|||
#import <AppKit/AppKit.h>
|
||||
#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<NSWindowDelegate>
|
||||
@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<WindowImplCocoa> 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<WindowImplCocoa>(kind,
|
||||
std::static_pointer_cast<WindowImplCocoa>(parentWindow));
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Application-wide APIs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void Exit() {
|
||||
[NSApp setDelegate:nil];
|
||||
[NSApp terminate:nil];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,14 @@
|
|||
|
||||
namespace SolveSpace {
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rendering
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
std::shared_ptr<ViewportCanvas> CreateRenderer() {
|
||||
return std::make_shared<CairoPixmapRenderer>();
|
||||
}
|
||||
|
||||
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<void()> onTrigger = NULL) override {
|
||||
return std::make_shared<MenuItemImplDummy>();
|
||||
}
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
return std::make_shared<MenuImplDummy>();
|
||||
}
|
||||
|
||||
void AddSeparator() override {}
|
||||
void PopUp() override {}
|
||||
void Clear() override {}
|
||||
};
|
||||
|
||||
MenuRef CreateMenu() {
|
||||
return std::make_shared<MenuImplDummy>();
|
||||
return std::shared_ptr<Menu>();
|
||||
}
|
||||
|
||||
class MenuBarImplDummy : public MenuBar {
|
||||
public:
|
||||
MenuRef AddSubMenu(const std::string &label) override {
|
||||
return std::make_shared<MenuImplDummy>();
|
||||
}
|
||||
void Clear() override {}
|
||||
void *NativePtr() override { return NULL; }
|
||||
};
|
||||
|
||||
MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||
*unique = false;
|
||||
return std::make_shared<MenuBarImplDummy>();
|
||||
return std::shared_ptr<MenuBar>();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Windows
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
|
||||
return std::shared_ptr<Window>();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// 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<ViewportCanvas> CreateRenderer() {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Graphics window
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void GetGraphicsWindowSize(int *w, int *h) {
|
||||
*w = *h = 600;
|
||||
}
|
||||
double GetScreenDpi() {
|
||||
return 72;
|
||||
}
|
||||
|
||||
void InvalidateGraphics() {
|
||||
}
|
||||
|
||||
std::shared_ptr<Pixmap> framebuffer;
|
||||
bool antialias = true;
|
||||
void PaintGraphics() {
|
||||
const Camera &camera = SS.GW.GetCamera();
|
||||
|
||||
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
|
||||
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<uint8_t>(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<Platform::Path> GetFontFiles() {
|
|||
return fontFiles;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Application lifecycle
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void RefreshLocale() {
|
||||
}
|
||||
|
||||
void ExitNow() {
|
||||
ssassert(false, "Not implemented");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,9 +3,24 @@
|
|||
//
|
||||
// Copyright 2018 whitequark
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "config.h"
|
||||
#include "solvespace.h"
|
||||
// Include after solvespace.h to avoid identifier clashes.
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
#include <commctrl.h>
|
||||
|
||||
#ifndef WM_DPICHANGED
|
||||
#define WM_DPICHANGED 0x02E0
|
||||
#endif
|
||||
|
||||
// We have our own CreateWindow.
|
||||
#undef CreateWindow
|
||||
|
||||
#if HAVE_OPENGL == 3
|
||||
#define EGLAPI /*static linkage*/
|
||||
#include <EGL/egl.h>
|
||||
#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<MenuBarImplWin32>();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// 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<MenuBarImplWin32> 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<WindowImplWin32> 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<MenuBarImplWin32>(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<WindowImplWin32>(kind,
|
||||
std::static_pointer_cast<WindowImplWin32>(parentWindow));
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Application-wide APIs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void Exit() {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Pixmap> 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> pixmap;
|
||||
|
||||
cairo_surface_t *surface = NULL;
|
||||
|
||||
void Init();
|
||||
void Clear() override;
|
||||
|
||||
std::shared_ptr<Pixmap> ReadFrame() override;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// 3d renderers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
|
|
@ -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<Pixmap> 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>();
|
||||
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<uint8_t>(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<Pixmap> CairoPixmapRenderer::ReadFrame() {
|
||||
std::shared_ptr<Pixmap> result = pixmap->Copy();
|
||||
result->ConvertTo(Pixmap::Format::RGBA);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -705,7 +705,9 @@ void OpenGl1Renderer::InvalidatePixmap(std::shared_ptr<const Pixmap> 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();
|
||||
|
|
|
@ -541,7 +541,9 @@ void OpenGl2Renderer::DrawPixmap(std::shared_ptr<const Pixmap> 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<Pixmap> OpenGl2Renderer::ReadFrame() {
|
||||
std::shared_ptr<Pixmap> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -354,6 +354,16 @@ std::shared_ptr<Pixmap> Pixmap::Create(Format format, size_t width, size_t heigh
|
|||
return pixmap;
|
||||
}
|
||||
|
||||
std::shared_ptr<Pixmap> Pixmap::Copy() {
|
||||
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
|
||||
pixmap->format = format;
|
||||
pixmap->width = width;
|
||||
pixmap->height = height;
|
||||
pixmap->stride = stride;
|
||||
pixmap->data = data;
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// ASCII sequence parsing
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -938,7 +948,7 @@ 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) &&
|
||||
if(camera.gridFit && !(u.WithMagnitude(1).Equals(camera.projRight) &&
|
||||
v.WithMagnitude(1).Equals(camera.projUp))) {
|
||||
return Trace(forCapHeight, o, u, v, str, traceEdge);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ public:
|
|||
|
||||
void ConvertTo(Format newFormat);
|
||||
void SetPixel(size_t x, size_t y, RgbaColor color);
|
||||
|
||||
std::shared_ptr<Pixmap> Copy();
|
||||
};
|
||||
|
||||
class BitmapFont {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<Platform::Path> 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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
|
|
164
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() {
|
||||
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<ViewportCanvas> 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<Pixmap> 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.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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
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;
|
||||
|
|
37
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<ViewportCanvas> 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<BatchCanvas> 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<Entity *> &entities,
|
||||
const std::vector<Constraint *> &constraints,
|
||||
const std::vector<hEntity> &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,12 +833,13 @@ 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);
|
||||
void MouseLeftDown(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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
@ -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 = {};
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
namespace SolveSpace {
|
||||
// These are defined in headless.cpp, and aren't exposed in solvespace.h.
|
||||
extern std::vector<Platform::Path> fontFiles;
|
||||
extern bool antialias;
|
||||
extern std::shared_ptr<Pixmap> 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<Pixmap> 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<Pixmap> 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<Pixmap> 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());
|
||||
|
||||
|
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |