diff --git a/src/draw.cpp b/src/draw.cpp index 8fb4b3b7..e7e007cb 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -882,13 +882,13 @@ void GraphicsWindow::Paint() { } // If we've had a screenshot requested, take it now, before the UI is overlaid. - if(!SS.screenshotFile.empty()) { - FILE *f = ssfopen(SS.screenshotFile, "wb"); + if(!SS.screenshotFile.IsEmpty()) { + FILE *f = OpenFile(SS.screenshotFile, "wb"); if(!f || !canvas->ReadFrame()->WritePng(f, /*flip=*/true)) { - Error("Couldn't write to '%s'", SS.screenshotFile.c_str()); + Error("Couldn't write to '%s'", SS.screenshotFile.raw.c_str()); } if(f) fclose(f); - SS.screenshotFile.clear(); + SS.screenshotFile.Clear(); } // And finally the toolbar. diff --git a/src/export.cpp b/src/export.cpp index c10567d5..bafd6852 100644 --- a/src/export.cpp +++ b/src/export.cpp @@ -8,7 +8,7 @@ //----------------------------------------------------------------------------- #include "solvespace.h" -void SolveSpaceUI::ExportSectionTo(const std::string &filename) { +void SolveSpaceUI::ExportSectionTo(const Platform::Path &filename) { Vector gn = (SS.GW.projRight).Cross(SS.GW.projUp); gn = gn.WithMagnitude(1); @@ -165,7 +165,7 @@ public: } }; -void SolveSpaceUI::ExportViewOrWireframeTo(const std::string &filename, bool exportWireframe) { +void SolveSpaceUI::ExportViewOrWireframeTo(const Platform::Path &filename, bool exportWireframe) { int i; SEdgeList edges = {}; SBezierList beziers = {}; @@ -605,29 +605,29 @@ double VectorFileWriter::MmToPts(double mm) { return (mm/25.4)*72; } -VectorFileWriter *VectorFileWriter::ForFile(const std::string &filename) { +VectorFileWriter *VectorFileWriter::ForFile(const Platform::Path &filename) { VectorFileWriter *ret; bool needOpen = true; - if(FilenameHasExtension(filename, ".dxf")) { + if(filename.HasExtension("dxf")) { static DxfFileWriter DxfWriter; ret = &DxfWriter; needOpen = false; - } else if(FilenameHasExtension(filename, ".ps") || FilenameHasExtension(filename, ".eps")) { + } else if(filename.HasExtension("ps") || filename.HasExtension("eps")) { static EpsFileWriter EpsWriter; ret = &EpsWriter; - } else if(FilenameHasExtension(filename, ".pdf")) { + } else if(filename.HasExtension("pdf")) { static PdfFileWriter PdfWriter; ret = &PdfWriter; - } else if(FilenameHasExtension(filename, ".svg")) { + } else if(filename.HasExtension("svg")) { static SvgFileWriter SvgWriter; ret = &SvgWriter; - } else if(FilenameHasExtension(filename, ".plt")||FilenameHasExtension(filename, ".hpgl")) { + } else if(filename.HasExtension("plt") || filename.HasExtension("hpgl")) { static HpglFileWriter HpglWriter; ret = &HpglWriter; - } else if(FilenameHasExtension(filename, ".step")||FilenameHasExtension(filename, ".stp")) { + } else if(filename.HasExtension("step") || filename.HasExtension("stp")) { static Step2dFileWriter Step2dWriter; ret = &Step2dWriter; - } else if(FilenameHasExtension(filename, ".txt")||FilenameHasExtension(filename, ".ngc")) { + } else if(filename.HasExtension("txt") || filename.HasExtension("ngc")) { static GCodeFileWriter GCodeWriter; ret = &GCodeWriter; } else { @@ -635,15 +635,15 @@ VectorFileWriter *VectorFileWriter::ForFile(const std::string &filename) { "filename '%s'; try " ".step, .stp, .dxf, .svg, .plt, .hpgl, .pdf, .txt, .ngc, " ".eps, or .ps.", - filename.c_str()); + filename.raw.c_str()); return NULL; } ret->filename = filename; if(!needOpen) return ret; - FILE *f = ssfopen(filename, "wb"); + FILE *f = OpenFile(filename, "wb"); if(!f) { - Error("Couldn't write to '%s'", filename.c_str()); + Error("Couldn't write to '%s'", filename.raw.c_str()); return NULL; } ret->f = f; @@ -793,7 +793,7 @@ void VectorFileWriter::BezierAsNonrationalCubic(SBezier *sb, int depth) { //----------------------------------------------------------------------------- // Export a triangle mesh, in the requested format. //----------------------------------------------------------------------------- -void SolveSpaceUI::ExportMeshTo(const std::string &filename) { +void SolveSpaceUI::ExportMeshTo(const Platform::Path &filename) { SS.exportMode = true; GenerateAll(Generate::ALL); @@ -806,33 +806,33 @@ void SolveSpaceUI::ExportMeshTo(const std::string &filename) { return; } - FILE *f = ssfopen(filename, "wb"); + FILE *f = OpenFile(filename, "wb"); if(!f) { - Error("Couldn't write to '%s'", filename.c_str()); + Error("Couldn't write to '%s'", filename.raw.c_str()); return; } ShowNakedEdges(/*reportOnlyWhenNotOkay=*/true); - if(FilenameHasExtension(filename, ".stl")) { + if(filename.HasExtension("stl")) { ExportMeshAsStlTo(f, m); - } else if(FilenameHasExtension(filename, ".obj")) { - std::string mtlFilename = filename.substr(0, filename.length() - 4) + ".mtl"; - FILE *fMtl = ssfopen(mtlFilename, "wb"); + } else if(filename.HasExtension("obj")) { + Platform::Path mtlFilename = filename.WithExtension("mtl"); + FILE *fMtl = OpenFile(mtlFilename, "wb"); if(!fMtl) { - Error("Couldn't write to '%s'", filename.c_str()); + Error("Couldn't write to '%s'", filename.raw.c_str()); return; } - fprintf(f, "mtllib %s\n", Basename(mtlFilename).c_str()); + fprintf(f, "mtllib %s\n", mtlFilename.FileName().c_str()); ExportMeshAsObjTo(f, fMtl, m); fclose(fMtl); - } else if(FilenameHasExtension(filename, ".js") || - FilenameHasExtension(filename, ".html")) { + } else if(filename.HasExtension("js") || + filename.HasExtension("html")) { SOutlineList *e = &(SK.GetGroup(SS.GW.activeGroup)->displayOutlines); ExportMeshAsThreeJsTo(f, filename, m, e); } else { Error("Can't identify output file type from file extension of " - "filename '%s'; try .stl, .obj, .js, .html.", filename.c_str()); + "filename '%s'; try .stl, .obj, .js, .html.", filename.raw.c_str()); } fclose(f); @@ -931,7 +931,7 @@ void SolveSpaceUI::ExportMeshAsObjTo(FILE *fObj, FILE *fMtl, SMesh *sm) { //----------------------------------------------------------------------------- // Export the mesh as a JavaScript script, which is compatible with Three.js. //----------------------------------------------------------------------------- -void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, +void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename, SMesh *sm, SOutlineList *sol) { SPointList spl = {}; @@ -986,14 +986,14 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, double largerBoundXY = max((bndh.x - bndl.x), (bndh.y - bndl.y)); double largerBoundZ = max(largerBoundXY, (bndh.z - bndl.z + 1)); - std::string basename = Basename(filename, /*stripExtension=*/true); + std::string basename = filename.FileStem(); for(size_t i = 0; i < basename.length(); i++) { if(!(isalnum(basename[i]) || ((unsigned)basename[i] >= 0x80))) { basename[i] = '_'; } } - if(FilenameHasExtension(filename, "html")) { + if(filename.HasExtension("html")) { fprintf(f, htmlbegin, LoadStringFromGzip("threejs/three-r76.js.gz").c_str(), LoadStringFromGzip("threejs/hammer-2.0.8.js.gz").c_str(), @@ -1089,7 +1089,7 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, fputs(" ]\n};\n", f); - if(FilenameHasExtension(filename, "html")) { + if(filename.HasExtension("html")) { fprintf(f, htmlend, basename.c_str(), SS.GW.scale, @@ -1105,7 +1105,7 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, // Export a view of the model as an image; we just take a screenshot, by // rendering the view in the usual way and then copying the pixels. //----------------------------------------------------------------------------- -void SolveSpaceUI::ExportAsPngTo(const std::string &filename) { +void SolveSpaceUI::ExportAsPngTo(const Platform::Path &filename) { screenshotFile = filename; // The rest of the work is done in the next redraw. InvalidateGraphics(); diff --git a/src/exportstep.cpp b/src/exportstep.cpp index af4270e2..970c925f 100644 --- a/src/exportstep.cpp +++ b/src/exportstep.cpp @@ -292,7 +292,7 @@ void StepFileWriter::WriteFooter() { ); } -void StepFileWriter::ExportSurfacesTo(const std::string &filename) { +void StepFileWriter::ExportSurfacesTo(const Platform::Path &filename) { Group *g = SK.GetGroup(SS.GW.activeGroup); SShell *shell = &(g->runningShell); @@ -305,9 +305,9 @@ void StepFileWriter::ExportSurfacesTo(const std::string &filename) { return; } - f = ssfopen(filename, "wb"); + f = OpenFile(filename, "wb"); if(!f) { - Error("Couldn't write to '%s'", filename.c_str()); + Error("Couldn't write to '%s'", filename.raw.c_str()); return; } diff --git a/src/exportvector.cpp b/src/exportvector.cpp index 5ab335d0..8d637d20 100644 --- a/src/exportvector.cpp +++ b/src/exportvector.cpp @@ -582,7 +582,7 @@ void DxfFileWriter::FinishAndCloseFile() { constraint = NULL; if(!WriteFile(filename, stream.str())) { - Error("Couldn't write to '%s'", filename.c_str()); + Error("Couldn't write to '%s'", filename.raw.c_str()); return; } diff --git a/src/file.cpp b/src/file.cpp index 77588268..99ac2c7d 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -116,8 +116,8 @@ const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { { 'g', "Group.allDimsReference", 'b', &(SS.sv.g.allDimsReference) }, { 'g', "Group.scale", 'f', &(SS.sv.g.scale) }, { 'g', "Group.remap", 'M', &(SS.sv.g.remap) }, - { 'g', "Group.impFile", 'S', &(SS.sv.g.linkFile) }, - { 'g', "Group.impFileRel", 'S', &(SS.sv.g.linkFileRel) }, + { 'g', "Group.impFile", 'i', NULL }, + { 'g', "Group.impFileRel", 'P', &(SS.sv.g.linkFile) }, { 'p', "Param.h.v.", 'x', &(SS.sv.p.h.v) }, { 'p', "Param.val", 'f', &(SS.sv.p.val) }, @@ -209,6 +209,7 @@ const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { struct SAVEDptr { IdList &M() { return *((IdList *)this); } std::string &S() { return *((std::string *)this); } + Platform::Path &P() { return *((Platform::Path *)this); } bool &b() { return *((bool *)this); } RgbaColor &c() { return *((RgbaColor *)this); } int &d() { return *((int *)this); } @@ -216,7 +217,7 @@ struct SAVEDptr { uint32_t &x() { return *((uint32_t *)this); } }; -void SolveSpaceUI::SaveUsingTable(int type) { +void SolveSpaceUI::SaveUsingTable(const Platform::Path &filename, int type) { int i; for(i = 0; SAVED[i].type != 0; i++) { if(SAVED[i].type != type) continue; @@ -225,9 +226,11 @@ void SolveSpaceUI::SaveUsingTable(int type) { SAVEDptr *p = (SAVEDptr *)SAVED[i].ptr; // Any items that aren't specified are assumed to be zero if(fmt == 'S' && p->S().empty()) continue; + if(fmt == 'P' && p->P().IsEmpty()) continue; if(fmt == 'd' && p->d() == 0) continue; if(fmt == 'f' && EXACT(p->f() == 0.0)) continue; if(fmt == 'x' && p->x() == 0) continue; + if(fmt == 'i') continue; fprintf(fh, "%s=", SAVED[i].desc); switch(fmt) { @@ -238,6 +241,15 @@ void SolveSpaceUI::SaveUsingTable(int type) { case 'f': fprintf(fh, "%.20f", p->f()); break; case 'x': fprintf(fh, "%08x", p->x()); break; + case 'P': { + if(!p->P().IsEmpty()) { + Platform::Path relativePath = p->P().RelativeTo(filename.Parent()); + ssassert(!relativePath.IsEmpty(), "Cannot relativize path"); + fprintf(fh, "%s", relativePath.ToPortable().c_str()); + } + break; + } + case 'M': { int j; fprintf(fh, "{\n"); @@ -250,23 +262,32 @@ void SolveSpaceUI::SaveUsingTable(int type) { break; } + case 'i': break; + default: ssassert(false, "Unexpected value format"); } fprintf(fh, "\n"); } } -bool SolveSpaceUI::SaveToFile(const std::string &filename) { - // Make sure all the entities are regenerated up to date, since they - // will be exported. We reload the linked files because that rewrites - // the linkFileRel for our possibly-new filename. +bool SolveSpaceUI::SaveToFile(const Platform::Path &filename) { + // Make sure all the entities are regenerated up to date, since they will be exported. SS.ScheduleShowTW(); - SS.ReloadAllImported(filename); SS.GenerateAll(SolveSpaceUI::Generate::ALL); - fh = ssfopen(filename, "wb"); + for(Group &g : SK.group) { + if(g.type != Group::Type::LINKED) continue; + + if(g.linkFile.RelativeTo(filename).IsEmpty()) { + Error("This sketch links the sketch '%s'; it can only be saved " + "on the same volume.", g.linkFile.raw.c_str()); + return false; + } + } + + fh = OpenFile(filename, "wb"); if(!fh) { - Error("Couldn't write to file '%s'", filename.c_str()); + Error("Couldn't write to file '%s'", filename.raw.c_str()); return false; } @@ -275,39 +296,39 @@ bool SolveSpaceUI::SaveToFile(const std::string &filename) { int i, j; for(i = 0; i < SK.group.n; i++) { sv.g = SK.group.elem[i]; - SaveUsingTable('g'); + SaveUsingTable(filename, 'g'); fprintf(fh, "AddGroup\n\n"); } for(i = 0; i < SK.param.n; i++) { sv.p = SK.param.elem[i]; - SaveUsingTable('p'); + SaveUsingTable(filename, 'p'); fprintf(fh, "AddParam\n\n"); } for(i = 0; i < SK.request.n; i++) { sv.r = SK.request.elem[i]; - SaveUsingTable('r'); + SaveUsingTable(filename, 'r'); fprintf(fh, "AddRequest\n\n"); } for(i = 0; i < SK.entity.n; i++) { (SK.entity.elem[i]).CalculateNumerical(/*forExport=*/true); sv.e = SK.entity.elem[i]; - SaveUsingTable('e'); + SaveUsingTable(filename, 'e'); fprintf(fh, "AddEntity\n\n"); } for(i = 0; i < SK.constraint.n; i++) { sv.c = SK.constraint.elem[i]; - SaveUsingTable('c'); + SaveUsingTable(filename, 'c'); fprintf(fh, "AddConstraint\n\n"); } for(i = 0; i < SK.style.n; i++) { sv.s = SK.style.elem[i]; if(sv.s.h.v >= Style::FIRST_CUSTOM) { - SaveUsingTable('s'); + SaveUsingTable(filename, 's'); fprintf(fh, "AddStyle\n\n"); } } @@ -373,7 +394,7 @@ bool SolveSpaceUI::SaveToFile(const std::string &filename) { return true; } -void SolveSpaceUI::LoadUsingTable(char *key, char *val) { +void SolveSpaceUI::LoadUsingTable(const Platform::Path &filename, char *key, char *val) { int i; for(i = 0; SAVED[i].type != 0; i++) { if(strcmp(SAVED[i].desc, key)==0) { @@ -386,15 +407,16 @@ void SolveSpaceUI::LoadUsingTable(char *key, char *val) { case 'f': p->f() = atof(val); break; case 'x': sscanf(val, "%x", &u); p->x()= u; break; + case 'P': { + p->P() = filename.Parent().Join(Platform::Path::FromPortable(val)); + break; + } + case 'c': sscanf(val, "%x", &u); p->c() = RgbaColor::FromPackedInt(u); break; - case 'P': - p->S() = val; - break; - case 'M': { // Don't clear this list! When the group gets added, it // makes a shallow copy, so that would result in us @@ -417,6 +439,8 @@ void SolveSpaceUI::LoadUsingTable(char *key, char *val) { break; } + case 'i': break; + default: ssassert(false, "Unexpected value format"); } break; @@ -427,13 +451,13 @@ void SolveSpaceUI::LoadUsingTable(char *key, char *val) { } } -bool SolveSpaceUI::LoadFromFile(const std::string &filename, bool canCancel) { +bool SolveSpaceUI::LoadFromFile(const Platform::Path &filename, bool canCancel) { allConsistent = false; fileLoadError = false; - fh = ssfopen(filename, "rb"); + fh = OpenFile(filename, "rb"); if(!fh) { - Error("Couldn't read from file '%s'", filename.c_str()); + Error("Couldn't read from file '%s'", filename.raw.c_str()); return false; } @@ -458,7 +482,7 @@ bool SolveSpaceUI::LoadFromFile(const std::string &filename, bool canCancel) { if(e) { *e = '\0'; char *key = line, *val = e+1; - LoadUsingTable(key, val); + LoadUsingTable(filename, key, val); } else if(strcmp(line, "AddGroup")==0) { // legacy files have a spurious dependency between linked groups // and their parent groups, remove @@ -660,13 +684,13 @@ void SolveSpaceUI::UpgradeLegacyData() { oldParam.Clear(); } -bool SolveSpaceUI::LoadEntitiesFromFile(const std::string &filename, EntityList *le, +bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh) { SSurface srf = {}; SCurve crv = {}; - fh = ssfopen(filename, "rb"); + fh = OpenFile(filename, "rb"); if(!fh) return false; le->Clear(); @@ -687,7 +711,7 @@ bool SolveSpaceUI::LoadEntitiesFromFile(const std::string &filename, EntityList if(e) { *e = '\0'; char *key = line, *val = e+1; - LoadUsingTable(key, val); + LoadUsingTable(filename, key, val); } else if(strcmp(line, "AddGroup")==0) { // Don't leak memory; these get allocated whether we want them // or not. @@ -795,173 +819,51 @@ bool SolveSpaceUI::LoadEntitiesFromFile(const std::string &filename, EntityList return true; } -//----------------------------------------------------------------------------- -// Handling of the relative-absolute path transformations for links -//----------------------------------------------------------------------------- -static std::vector Split(const std::string &haystack, const std::string &needle) +bool SolveSpaceUI::ReloadAllImported(const Platform::Path &filename, bool canCancel) { - std::vector result; + std::map linkMap; - size_t oldpos = 0, pos = 0; - while(true) { - oldpos = pos; - pos = haystack.find(needle, pos); - if(pos == std::string::npos) break; - result.push_back(haystack.substr(oldpos, pos - oldpos)); - pos += needle.length(); - } - - if(oldpos != haystack.length() - 1) - result.push_back(haystack.substr(oldpos)); - - return result; -} - -static std::string Join(const std::vector &parts, const std::string &separator) -{ - bool first = true; - std::string result; - for(auto &part : parts) { - if(!first) result += separator; - result += part; - first = false; - } - return result; -} - -static std::string MakePathRelative(const std::string &base, const std::string &path) -{ - std::vector baseParts = Split(base, PATH_SEP), - pathParts = Split(path, PATH_SEP), - resultParts; - baseParts.pop_back(); - - size_t common; - for(common = 0; common < baseParts.size() && common < pathParts.size(); common++) { - if(!PathEqual(baseParts[common], pathParts[common])) - break; - } - - for(size_t i = common; i < baseParts.size(); i++) - resultParts.push_back(".."); - - resultParts.insert(resultParts.end(), - pathParts.begin() + common, pathParts.end()); - - return Join(resultParts, PATH_SEP); -} - -static std::string MakePathAbsolute(const std::string &base, const std::string &path) -{ - std::vector resultParts = Split(base, PATH_SEP), - pathParts = Split(path, PATH_SEP); - resultParts.pop_back(); - - for(auto &part : pathParts) { - if(part == ".") { - /* do nothing */ - } else if(part == "..") { - ssassert(!resultParts.empty(), "Relative path pointing outside of root directory"); - resultParts.pop_back(); - } else { - resultParts.push_back(part); - } - } - - return Join(resultParts, PATH_SEP); -} - -static void PathSepNormalize(std::string &filename) -{ - for(size_t i = 0; i < filename.length(); i++) { - if(filename[i] == '\\') - filename[i] = '/'; - } -} - -bool SolveSpaceUI::ReloadAllImported(const std::string &filename, bool canCancel) -{ - std::string saveFile = filename.empty() ? SS.saveFile : filename; - std::map linkMap; allConsistent = false; + for(Group &g : SK.group) { + if(g.type != Group::Type::LINKED) continue; - int i; - for(i = 0; i < SK.group.n; i++) { - Group *g = &(SK.group.elem[i]); - if(g->type != Group::Type::LINKED) continue; + g.impEntity.Clear(); + g.impMesh.Clear(); + g.impShell.Clear(); - if(isalpha(g->linkFile[0]) && g->linkFile[1] == ':') { - // Make sure that g->linkFileRel always contains a relative path - // in an UNIX format, even after we load an old file which had - // the path in Windows format - PathSepNormalize(g->linkFileRel); + // If we prompted for this specific file before, don't ask again. + if(linkMap.count(g.linkFile)) { + g.linkFile = linkMap[g.linkFile]; } - g->impEntity.Clear(); - g->impMesh.Clear(); - g->impShell.Clear(); - - if(linkMap.count(g->linkFile)) { - std::string newPath = linkMap[g->linkFile]; - if(!newPath.empty()) - g->linkFile = newPath; - } - - // In a newly created group we only have an absolute path. - if(!g->linkFileRel.empty()) { - std::string rel = PathSepUnixToPlatform(g->linkFileRel); - std::string fromRel = MakePathAbsolute(saveFile, rel); - FILE *test = ssfopen(fromRel, "rb"); - if(test) { - fclose(test); - // Okay, exists; update the absolute path. - g->linkFile = fromRel; - } else { - // It doesn't exist. Perhaps the file was moved but the tree wasn't, and we - // can use the absolute filename to get us back. The relative path will be - // updated below. - } - } - -try_load_file: - if(LoadEntitiesFromFile(g->linkFile, &(g->impEntity), &(g->impMesh), &(g->impShell))) - { - if(!saveFile.empty()) { - // Record the linked file's name relative to our filename; - // if the entire tree moves, then everything will still work - std::string rel = MakePathRelative(saveFile, g->linkFile); - g->linkFileRel = PathSepPlatformToUnix(rel); - } else { - // We're not yet saved, so can't make it absolute. - // This will only be used for display purposes, as saveFile - // is always nonempty when we are actually writing anything. - g->linkFileRel = g->linkFile; - } - } else if(!linkMap.count(g->linkFile)) { - switch(LocateImportedFileYesNoCancel(g->linkFileRel, canCancel)) { +try_again: + if(LoadEntitiesFromFile(g.linkFile, &g.impEntity, &g.impMesh, &g.impShell)) { + // We loaded the data, good. + } else if(linkMap.count(g.linkFile) == 0) { + // The file was moved; prompt the user for its new location. + switch(LocateImportedFileYesNoCancel(g.linkFile.RelativeTo(filename), canCancel)) { case DIALOG_YES: { - std::string oldImpFile = g->linkFile; - if(!GetOpenFile(&g->linkFile, "", SlvsFileFilter)) { - if(canCancel) - return false; - break; + Platform::Path newLinkFile; + if(GetOpenFile(&newLinkFile, "", SlvsFileFilter)) { + linkMap[g.linkFile] = newLinkFile; + g.linkFile = newLinkFile; + goto try_again; } else { - linkMap[oldImpFile] = g->linkFile; - goto try_load_file; + if(canCancel) return false; + break; } } case DIALOG_NO: - linkMap[g->linkFile] = ""; - /* Geometry will be pruned by GenerateAll(). */ + linkMap[g.linkFile].Clear(); + // Geometry will be pruned by GenerateAll(). break; case DIALOG_CANCEL: return false; } } else { - // User was already asked to and refused to locate a missing - // linked file. + // User was already asked to and refused to locate a missing linked file. } } diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index 1c445808..e634e9e8 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -943,7 +943,7 @@ void GraphicsWindow::MenuEdit(Command id) { break; case Command::REGEN_ALL: - SS.ReloadAllImported(); + SS.ReloadAllImported(SS.saveFile); SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); SS.ScheduleShowTW(); break; diff --git a/src/group.cpp b/src/group.cpp index 041635ba..4671c86c 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -229,13 +229,13 @@ void Group::MenuGroup(Command id) { case Command::GROUP_LINK: { g.type = Type::LINKED; g.meshCombine = CombineAs::ASSEMBLE; - if(g.linkFile.empty()) { + if(g.linkFile.IsEmpty()) { if(!GetOpenFile(&g.linkFile, "", SlvsFileFilter)) return; } // Assign the default name of the group based on the name of // the linked file. - g.name = Basename(g.linkFile, /*stripExtension=*/true); + g.name = g.linkFile.FileStem(); for(size_t i = 0; i < g.name.length(); i++) { if(!(isalnum(g.name[i]) || (unsigned)g.name[i] >= 0x80)) { // convert punctuation to dashes @@ -274,7 +274,7 @@ void Group::MenuGroup(Command id) { Group *gg = SK.GetGroup(g.h); if(gg->type == Type::LINKED) { - SS.ReloadAllImported(); + SS.ReloadAllImported(SS.saveFile); } gg->clean = false; SS.GW.activeGroup = gg->h; diff --git a/src/importdxf.cpp b/src/importdxf.cpp index 38b0aab0..7eb36168 100644 --- a/src/importdxf.cpp +++ b/src/importdxf.cpp @@ -1068,13 +1068,13 @@ public: } }; -static void ImportDwgDxf(const std::string &filename, +static void ImportDwgDxf(const Platform::Path &filename, std::function read) { - std::string fileType = ToUpper(Extension(filename)); + std::string fileType = ToUpper(filename.Extension()); std::string data; if(!ReadFile(filename, &data)) { - Error("Couldn't read from '%s'", filename.c_str()); + Error("Couldn't read from '%s'", filename.raw.c_str()); return; } @@ -1107,14 +1107,14 @@ static void ImportDwgDxf(const std::string &filename, } } -void ImportDxf(const std::string &filename) { +void ImportDxf(const Platform::Path &filename) { ImportDwgDxf(filename, [](const std::string &data, DRW_Interface *intf) { std::stringstream stream(data); return dxfRW().read(stream, intf, /*ext=*/false); }); } -void ImportDwg(const std::string &filename) { +void ImportDwg(const Platform::Path &filename) { ImportDwgDxf(filename, [](const std::string &data, DRW_Interface *intf) { std::stringstream stream(data); return dwgR().read(stream, intf, /*ext=*/false); diff --git a/src/platform/climain.cpp b/src/platform/climain.cpp index 318494a3..52881ac8 100644 --- a/src/platform/climain.cpp +++ b/src/platform/climain.cpp @@ -93,13 +93,13 @@ static bool RunCommand(const std::vector args) { } } - std::function runner; + std::function runner; - std::vector inputFiles; + std::vector inputFiles; auto ParseInputFile = [&](size_t &argn) { std::string arg = args[argn]; if(arg[0] != '-') { - inputFiles.push_back(arg); + inputFiles.push_back(Platform::Path::From(arg)); return true; } else return false; }; @@ -190,7 +190,7 @@ static bool RunCommand(const std::vector args) { return false; } - runner = [&](const std::string &output) { + runner = [&](const Platform::Path &output) { SS.GW.width = width; SS.GW.height = height; SS.GW.projRight = projRight; @@ -218,7 +218,7 @@ static bool RunCommand(const std::vector args) { return false; } - runner = [&](const std::string &output) { + runner = [&](const Platform::Path &output) { SS.GW.projRight = projRight; SS.GW.projUp = projUp; SS.exportChordTol = chordTol; @@ -235,7 +235,7 @@ static bool RunCommand(const std::vector args) { } } - runner = [&](const std::string &output) { + runner = [&](const Platform::Path &output) { SS.exportChordTol = chordTol; SS.ExportViewOrWireframeTo(output, /*exportWireframe=*/true); @@ -250,7 +250,7 @@ static bool RunCommand(const std::vector args) { } } - runner = [&](const std::string &output) { + runner = [&](const Platform::Path &output) { SS.exportChordTol = chordTol; SS.ExportMeshTo(output); @@ -264,7 +264,7 @@ static bool RunCommand(const std::vector args) { } } - runner = [&](const std::string &output) { + runner = [&](const Platform::Path &output) { StepFileWriter sfw = {}; sfw.ExportSurfacesTo(output); }; @@ -278,7 +278,7 @@ static bool RunCommand(const std::vector args) { outputPattern = "%.slvs"; - runner = [&](const std::string &output) { + runner = [&](const Platform::Path &output) { SS.SaveToFile(output); }; } else { @@ -300,25 +300,21 @@ static bool RunCommand(const std::vector args) { return false; } - for(const std::string &inputFile : inputFiles) { - std::string absInputFile = PathFromCurrentDirectory(inputFile); + for(const Platform::Path &inputFile : inputFiles) { + Platform::Path absInputFile = inputFile.Expand(/*fromCurrentDirectory=*/true); - std::string outputFile = outputPattern; - size_t replaceAt = outputFile.find('%'); + Platform::Path outputFile = Platform::Path::From(outputPattern); + size_t replaceAt = outputFile.raw.find('%'); if(replaceAt != std::string::npos) { - std::string outputSubst; - outputSubst = Dirname(inputFile); - if(!outputSubst.empty()) { - outputSubst += PATH_SEP; - } - outputSubst += Basename(inputFile, /*stripExtension=*/true); - outputFile.replace(replaceAt, 1, outputSubst); + Platform::Path outputSubst = inputFile.Parent(); + outputSubst = outputSubst.Join(inputFile.FileStem()); + outputFile.raw.replace(replaceAt, 1, outputSubst.raw); } - std::string absOutputFile = PathFromCurrentDirectory(outputFile); + Platform::Path absOutputFile = outputFile.Expand(/*fromCurrentDirectory=*/true); SS.Init(); if(!SS.LoadFromFile(absInputFile)) { - fprintf(stderr, "Cannot load '%s'!\n", inputFile.c_str()); + fprintf(stderr, "Cannot load '%s'!\n", inputFile.raw.c_str()); return false; } SS.AfterNewFile(); @@ -326,7 +322,7 @@ static bool RunCommand(const std::vector args) { SK.Clear(); SS.Clear(); - fprintf(stderr, "Written '%s'.\n", outputFile.c_str()); + fprintf(stderr, "Written '%s'.\n", outputFile.raw.c_str()); } return true; diff --git a/src/platform/cocoamain.mm b/src/platform/cocoamain.mm index e2676ff3..c1c03fed 100644 --- a/src/platform/cocoamain.mm +++ b/src/platform/cocoamain.mm @@ -474,9 +474,9 @@ void PaintGraphics() { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); } -void SetCurrentFilename(const std::string &filename) { - if(!filename.empty()) { - [GW setTitleWithRepresentedFilename:Wrap(filename)]; +void SetCurrentFilename(const Platform::Path &filename) { + if(!filename.IsEmpty()) { + [GW setTitleWithRepresentedFilename:Wrap(filename.raw)]; } else { [GW setTitle:Wrap(C_("title", "(new sketch)"))]; [GW setRepresentedFilename:@""]; @@ -705,18 +705,17 @@ static void RefreshRecentMenu(SolveSpace::Command cmd, SolveSpace::Command base) NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; [recent setSubmenu:menu]; - if(std::string(RecentFile[0]).empty()) { + if(RecentFile[0].IsEmpty()) { NSMenuItem *placeholder = [[NSMenuItem alloc] initWithTitle:Wrap(_("(no recent files)")) action:nil keyEquivalent:@""]; [placeholder setEnabled:NO]; [menu addItem:placeholder]; } else { for(size_t i = 0; i < MAX_RECENT; i++) { - if(std::string(RecentFile[i]).empty()) - break; + if(RecentFile[i].IsEmpty()) break; NSMenuItem *item = [[NSMenuItem alloc] - initWithTitle:[Wrap(RecentFile[i]) + initWithTitle:[Wrap(RecentFile[i].raw) stringByAbbreviatingWithTildeInPath] action:nil keyEquivalent:@""]; [item setTag:((uint32_t)base + i)]; @@ -743,7 +742,7 @@ bool MenuBarIsVisible() { /* Save/load */ -bool SolveSpace::GetOpenFile(std::string *file, const std::string &defExtension, +bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension, const FileFilter ssFilters[]) { NSOpenPanel *panel = [NSOpenPanel openPanel]; NSMutableArray *filters = [[NSMutableArray alloc] init]; @@ -756,8 +755,9 @@ bool SolveSpace::GetOpenFile(std::string *file, const std::string &defExtension, [panel setAllowedFileTypes:filters]; if([panel runModal] == NSFileHandlingPanelOKButton) { - *file = [[NSFileManager defaultManager] - fileSystemRepresentationWithPath:[[panel URL] path]]; + *filename = Platform::Path::From( + [[NSFileManager defaultManager] + fileSystemRepresentationWithPath:[[panel URL] path]]); return true; } else { return false; @@ -784,7 +784,7 @@ bool SolveSpace::GetOpenFile(std::string *file, const std::string &defExtension, } @end -bool SolveSpace::GetSaveFile(std::string *file, const std::string &defExtension, +bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExtension, const FileFilter ssFilters[]) { NSSavePanel *panel = [NSSavePanel savePanel]; @@ -823,22 +823,23 @@ bool SolveSpace::GetSaveFile(std::string *file, const std::string &defExtension, } [button selectItemAtIndex:extensionIndex]; - if(file->empty()) { + if(filename->IsEmpty()) { [panel setNameFieldStringValue: [Wrap(_("untitled")) stringByAppendingPathExtension:[extensions objectAtIndex:extensionIndex]]]; } else { [panel setDirectoryURL: - [NSURL fileURLWithPath:Wrap(Dirname(*file)) + [NSURL fileURLWithPath:Wrap(filename->Parent().raw) isDirectory:NO]]; [panel setNameFieldStringValue: - [Wrap(Basename(*file, /*stripExtension=*/true)) + [Wrap(filename->FileStem()) stringByAppendingPathExtension:[extensions objectAtIndex:extensionIndex]]]; } if([panel runModal] == NSFileHandlingPanelOKButton) { - *file = [[NSFileManager defaultManager] - fileSystemRepresentationWithPath:[[panel URL] path]]; + *filename = Platform::Path::From( + [[NSFileManager defaultManager] + fileSystemRepresentationWithPath:[[panel URL] path]]); return true; } else { return false; @@ -847,11 +848,11 @@ bool SolveSpace::GetSaveFile(std::string *file, const std::string &defExtension, SolveSpace::DialogChoice SolveSpace::SaveFileYesNoCancel() { NSAlert *alert = [[NSAlert alloc] init]; - if(!std::string(SolveSpace::SS.saveFile).empty()) { + if(!SolveSpace::SS.saveFile.IsEmpty()) { [alert setMessageText: [[@"Do you want to save the changes you made to the sketch “" stringByAppendingString: - [Wrap(SolveSpace::SS.saveFile) + [Wrap(SolveSpace::SS.saveFile.raw) stringByAbbreviatingWithTildeInPath]] stringByAppendingString:@"”?"]]; } else { @@ -891,10 +892,10 @@ SolveSpace::DialogChoice SolveSpace::LoadAutosaveYesNo() { } SolveSpace::DialogChoice SolveSpace::LocateImportedFileYesNoCancel( - const std::string &filename, bool canCancel) { + const Platform::Path &filename, bool canCancel) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText: - Wrap("The linked file “" + filename + "” is not present.")]; + Wrap("The linked file “" + filename.raw + "” is not present.")]; [alert setInformativeText: Wrap(_("Do you want to locate it manually?\n" "If you select “No”, any geometry that depends on " @@ -1148,8 +1149,8 @@ void SolveSpace::OpenWebsite(const char *url) { [NSURL URLWithString:[NSString stringWithUTF8String:url]]]; } -std::vector SolveSpace::GetFontFiles() { - std::vector fonts; +std::vector SolveSpace::GetFontFiles() { + std::vector fonts; NSArray *fontNames = [[NSFontManager sharedFontManager] availableFonts]; for(NSString *fontName in fontNames) { @@ -1157,8 +1158,9 @@ std::vector SolveSpace::GetFontFiles() { CTFontDescriptorCreateWithNameAndSize ((__bridge CFStringRef)fontName, 10.0); CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(fontRef, kCTFontURLAttribute); NSString *fontPath = [NSString stringWithString:[(NSURL *)CFBridgingRelease(url) path]]; - fonts.push_back([[NSFileManager defaultManager] - fileSystemRepresentationWithPath:fontPath]); + fonts.push_back( + Platform::Path::From([[NSFileManager defaultManager] + fileSystemRepresentationWithPath:fontPath])); } return fonts; @@ -1191,7 +1193,8 @@ std::vector SolveSpace::GetFontFiles() { } - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { - return SolveSpace::SS.OpenFile(SolveSpace::PathFromCurrentDirectory([filename UTF8String])); + SolveSpace::Platform::Path path = SolveSpace::Platform::Path::From([filename UTF8String]); + return SolveSpace::SS.Load(path.Expand(/*fromCurrentDirectory=*/true)); } - (IBAction)preferences:(id)sender { diff --git a/src/platform/gtkmain.cpp b/src/platform/gtkmain.cpp index 3d456a27..a1e5bca9 100644 --- a/src/platform/gtkmain.cpp +++ b/src/platform/gtkmain.cpp @@ -602,8 +602,8 @@ void PaintGraphics(void) { Glib::MainContext::get_default()->iteration(false); } -void SetCurrentFilename(const std::string &filename) { - GW->set_title(Title(filename.empty() ? C_("title", "(new sketch)") : filename.c_str())); +void SetCurrentFilename(const Platform::Path &filename) { + GW->set_title(Title(filename.IsEmpty() ? C_("title", "(new sketch)") : filename.raw.c_str())); } void ToggleFullScreen(void) { @@ -907,16 +907,16 @@ static void RefreshRecentMenu(Command cmd, Command base) { Gtk::Menu *menu = new Gtk::Menu; recent->set_submenu(*menu); - if(RecentFile[0].empty()) { + if(RecentFile[0].IsEmpty()) { Gtk::MenuItem *placeholder = new Gtk::MenuItem(_("(no recent files)")); placeholder->set_sensitive(false); menu->append(*placeholder); } else { for(size_t i = 0; i < MAX_RECENT; i++) { - if(RecentFile[i].empty()) + if(RecentFile[i].IsEmpty()) break; - RecentMenuItem *item = new RecentMenuItem(RecentFile[i], (uint32_t)base + i); + RecentMenuItem *item = new RecentMenuItem(RecentFile[i].raw, (uint32_t)base + i); menu->append(*item); } } @@ -962,10 +962,10 @@ static std::string ConvertFilters(std::string active, const FileFilter ssFilters return active; } -bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, +bool GetOpenFile(Platform::Path *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Open File"))); - chooser.set_filename(*filename); + chooser.set_filename(filename->raw); chooser.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); chooser.add_button(_("_Open"), Gtk::RESPONSE_OK); chooser.set_current_folder(CnfThawString("", "FileChooserPath")); @@ -974,7 +974,7 @@ bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, if(chooser.run() == Gtk::RESPONSE_OK) { CnfFreezeString(chooser.get_current_folder(), "FileChooserPath"); - *filename = chooser.get_filename(); + *filename = Platform::Path::From(chooser.get_filename()); return true; } else { return false; @@ -1000,17 +1000,11 @@ static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser) if(extension.length() > 2 && extension.substr(0, 2) == "*.") extension = extension.substr(2, extension.length() - 2); - std::string basename = Basename(chooser->get_filename()); - int dot = basename.rfind('.'); - if(dot >= 0) { - basename.replace(dot + 1, basename.length() - dot - 1, extension); - chooser->set_current_name(basename); - } else { - chooser->set_current_name(basename + "." + extension); - } + Platform::Path path = Platform::Path::From(chooser->get_filename()); + chooser->set_current_name(path.WithExtension(extension).FileName()); } -bool GetSaveFile(std::string *filename, const std::string &defExtension, +bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]) { Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Save File")), Gtk::FILE_CHOOSER_ACTION_SAVE); @@ -1020,13 +1014,12 @@ bool GetSaveFile(std::string *filename, const std::string &defExtension, std::string activeExtension = ConvertFilters(defExtension, filters, &chooser); - if(filename->empty()) { + if(filename->IsEmpty()) { chooser.set_current_folder(CnfThawString("", "FileChooserPath")); chooser.set_current_name(std::string(_("untitled")) + "." + activeExtension); } else { - chooser.set_current_folder(Dirname(*filename)); - chooser.set_current_name(Basename(*filename, /*stripExtension=*/true) + - "." + activeExtension); + chooser.set_current_folder(filename->Parent().raw); + chooser.set_current_name(filename->WithExtension(activeExtension).FileName()); } /* Gtk's dialog doesn't change the extension when you change the filter, @@ -1036,7 +1029,7 @@ bool GetSaveFile(std::string *filename, const std::string &defExtension, if(chooser.run() == Gtk::RESPONSE_OK) { CnfFreezeString(chooser.get_current_folder(), "FileChooserPath"); - *filename = chooser.get_filename(); + *filename = Platform::Path::From(chooser.get_filename()); return true; } else { return false; @@ -1087,10 +1080,10 @@ DialogChoice LoadAutosaveYesNo(void) { } } -DialogChoice LocateImportedFileYesNoCancel(const std::string &filename, +DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, bool canCancel) { Glib::ustring message = - "The linked file " + filename + " is not present.\n\n" + "The linked file " + filename.raw + " is not present.\n\n" "Do you want to locate it manually?\n\n" "If you select \"No\", any geometry that depends on " "the missing file will be removed."; @@ -1318,8 +1311,8 @@ void OpenWebsite(const char *url) { } /* fontconfig is already initialized by GTK */ -std::vector GetFontFiles() { - std::vector fonts; +std::vector GetFontFiles() { + std::vector fonts; FcPattern *pat = FcPatternCreate(); FcObjectSet *os = FcObjectSetBuild(FC_FILE, (char *)0); @@ -1327,8 +1320,7 @@ std::vector GetFontFiles() { for(int i = 0; i < fs->nfont; i++) { FcChar8 *filenameFC = FcPatternFormat(fs->fonts[i], (const FcChar8*) "%{file}"); - std::string filename = (char*) filenameFC; - fonts.push_back(filename); + fonts.push_back(Platform::Path::From((const char *)filenameFC)); FcStrFree(filenameFC); } @@ -1463,7 +1455,8 @@ int main(int argc, char** argv) { } /* Make sure the argument is valid UTF-8. */ - SS.OpenFile(PathFromCurrentDirectory(Glib::ustring(argv[1]))); + Glib::ustring arg(argv[1]); + SS.Load(Platform::Path::From(arg).Expand(/*fromCurrentDirectory=*/true)); } main.run(*GW); diff --git a/src/platform/headless.cpp b/src/platform/headless.cpp index efb12363..f37edf51 100644 --- a/src/platform/headless.cpp +++ b/src/platform/headless.cpp @@ -148,7 +148,7 @@ void PaintGraphics() { cairo_destroy(context); } -void SetCurrentFilename(const std::string &filename) { +void SetCurrentFilename(const Platform::Path &filename) { } void ToggleFullScreen() { } @@ -210,11 +210,11 @@ bool TextEditControlIsVisible() { // Dialogs //----------------------------------------------------------------------------- -bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, +bool GetOpenFile(Platform::Path *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { ssassert(false, "Not implemented"); } -bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty, +bool GetSaveFile(Platform::Path *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { ssassert(false, "Not implemented"); } @@ -224,7 +224,7 @@ DialogChoice SaveFileYesNoCancel() { DialogChoice LoadAutosaveYesNo() { ssassert(false, "Not implemented"); } -DialogChoice LocateImportedFileYesNoCancel(const std::string &filename, +DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, bool canCancel) { ssassert(false, "Not implemented"); } @@ -240,8 +240,8 @@ void OpenWebsite(const char *url) { // Resources //----------------------------------------------------------------------------- -std::vector fontFiles; -std::vector GetFontFiles() { +std::vector fontFiles; +std::vector GetFontFiles() { return fontFiles; } diff --git a/src/platform/platform.cpp b/src/platform/platform.cpp index e61f37e3..3e728bc5 100644 --- a/src/platform/platform.cpp +++ b/src/platform/platform.cpp @@ -1,24 +1,82 @@ //----------------------------------------------------------------------------- -// Common platform-dependent functionality. +// Platform-dependent functionality. // // Copyright 2017 whitequark //----------------------------------------------------------------------------- #if defined(__APPLE__) +// Include Apple headers before solvespace.h to avoid identifier clashes. # include +# include +# include #endif #include "solvespace.h" +#include "config.h" #if defined(WIN32) +// Conversely, include Microsoft headers after solvespace.h to avoid clashes. # include #else # include +# include #endif namespace SolveSpace { - -using namespace Platform; +namespace Platform { //----------------------------------------------------------------------------- -// Utility functions. +// UTF-8 ⟷ UTF-16 conversion, on Windows. +//----------------------------------------------------------------------------- + +#if defined(WIN32) + +std::string Narrow(const wchar_t *in) +{ + std::string out; + DWORD len = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL); + out.resize(len - 1); + ssassert(WideCharToMultiByte(CP_UTF8, 0, in, -1, &out[0], len, NULL, NULL), + "Invalid UTF-16"); + return out; +} + +std::string Narrow(const std::wstring &in) +{ + if(in == L"") return ""; + + std::string out; + out.resize(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(), + NULL, 0, NULL, NULL)); + ssassert(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(), + &out[0], (int)out.length(), NULL, NULL), + "Invalid UTF-16"); + return out; +} + +std::wstring Widen(const char *in) +{ + std::wstring out; + DWORD len = MultiByteToWideChar(CP_UTF8, 0, in, -1, NULL, 0); + out.resize(len - 1); + ssassert(MultiByteToWideChar(CP_UTF8, 0, in, -1, &out[0], len), + "Invalid UTF-8"); + return out; +} + +std::wstring Widen(const std::string &in) +{ + if(in == "") return L""; + + std::wstring out; + out.resize(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), NULL, 0)); + ssassert(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), + &out[0], (int)out.length()), + "Invalid UTF-8"); + return out; +} + +#endif + +//----------------------------------------------------------------------------- +// Path utility functions. //----------------------------------------------------------------------------- static std::vector Split(const std::string &joined, char separator) { @@ -70,7 +128,7 @@ Path Path::From(std::string raw) { Path Path::CurrentDirectory() { #if defined(WIN32) - // On Windows, ssfopen needs an absolute UNC path proper, so get that. + // On Windows, OpenFile needs an absolute UNC path proper, so get that. std::wstring rawW; rawW.resize(GetCurrentDirectoryW(0, NULL)); DWORD length = GetCurrentDirectoryW((int)rawW.length(), &rawW[0]); @@ -267,6 +325,7 @@ static std::string FilesystemNormalize(const std::string &str) { std::string normalizedStr; normalizedStr.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfStr)); CFStringGetFileSystemRepresentation(cfStr, &normalizedStr[0], normalizedStr.size()); + normalizedStr.erase(normalizedStr.find('\0')); return normalizedStr; #else return str; @@ -332,4 +391,182 @@ std::string Path::ToPortable() const { return Concat(Split(raw, SEPARATOR), '/'); } +//----------------------------------------------------------------------------- +// File manipulation. +//----------------------------------------------------------------------------- + +FILE *OpenFile(const Platform::Path &filename, const char *mode) { + ssassert(filename.raw.length() == strlen(filename.raw.c_str()), + "Unexpected null byte in middle of a path"); +#if defined(WIN32) + return _wfopen(Widen(filename.Expand().raw).c_str(), Widen(mode).c_str()); +#else + return fopen(filename.raw.c_str(), mode); +#endif +} + +void RemoveFile(const Platform::Path &filename) { + ssassert(filename.raw.length() == strlen(filename.raw.c_str()), + "Unexpected null byte in middle of a path"); +#if defined(WIN32) + _wremove(Widen(filename.Expand().raw).c_str()); +#else + remove(filename.raw.c_str()); +#endif +} + +bool ReadFile(const Platform::Path &filename, std::string *data) { + FILE *f = OpenFile(filename, "rb"); + if(f == NULL) return false; + + fseek(f, 0, SEEK_END); + data->resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(&(*data)[0], 1, data->size(), f); + fclose(f); + + return true; +} + +bool WriteFile(const Platform::Path &filename, const std::string &data) { + FILE *f = OpenFile(filename, "wb"); + if(f == NULL) return false; + + fwrite(&data[0], 1, data.size(), f); + fclose(f); + + return true; +} + +//----------------------------------------------------------------------------- +// Loading resources, on Windows. +//----------------------------------------------------------------------------- + +#if defined(WIN32) + +const void *LoadResource(const std::string &name, size_t *size) { + HRSRC hres = FindResourceW(NULL, Widen(name).c_str(), RT_RCDATA); + ssassert(hres != NULL, "Cannot find resource"); + HGLOBAL res = ::LoadResource(NULL, hres); + ssassert(res != NULL, "Cannot load resource"); + + *size = SizeofResource(NULL, hres); + return LockResource(res); +} + +#endif + +//----------------------------------------------------------------------------- +// Loading resources, on *nix. +//----------------------------------------------------------------------------- + +#if defined(__APPLE__) + +static Platform::Path PathFromCFURL(CFURLRef cfUrl) { + Path path; + CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle); + path.raw.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfPath)); + CFStringGetFileSystemRepresentation(cfPath, &path.raw[0], path.raw.size()); + path.raw.erase(path.raw.find('\0')); + CFRelease(cfPath); + return path; +} + +static Platform::Path ResourcePath(const std::string &name) { + Path path; + + // First, try to get the URL from the bundle. + CFStringRef cfName = CFStringCreateWithCString(kCFAllocatorDefault, name.c_str(), + kCFStringEncodingUTF8); + CFURLRef cfUrl = CFBundleCopyResourceURL(CFBundleGetMainBundle(), cfName, NULL, NULL); + if(cfUrl != NULL) { + path = PathFromCFURL(cfUrl); + CFRelease(cfUrl); + } + CFRelease(cfName); + + if(!path.IsEmpty()) return path; + + // If that failed, it means we aren't running from the bundle. + // Reference off the executable path, then. + cfUrl = CFBundleCopyExecutableURL(CFBundleGetMainBundle()); + if(cfUrl != NULL) { + path = PathFromCFURL(cfUrl).Parent().Parent().Join("res"); + path = path.Join(Path::FromPortable(name)); + CFRelease(cfUrl); + } + + return path; +} + +#elif !defined(WIN32) + +# if defined(__linux__) +static const char *selfSymlink = "/proc/self/exe"; +# elif defined(__NetBSD__) +static const char *selfSymlink = "/proc/curproc/exe" +# elif defined(__OpenBSD__) || defined(__FreeBSD__) +static const char *selfSymlink = "/proc/curproc/file"; +# else +static const char *selfSymlink = ""; +# endif + +static Platform::Path FindLocalResourceDir() { + // Find out the path to the running binary. + Platform::Path selfPath; + char *expandedSelfPath = realpath(selfSymlink, NULL); + if(expandedSelfPath != NULL) { + selfPath = Path::From(expandedSelfPath); + } + free(expandedSelfPath); + + Platform::Path resourceDir; + if(selfPath.IsEmpty()) { + // We don't know how to find the local resource directory on this platform, + // so use the global one (by returning an empty string). + return Path::From(UNIX_DATADIR); + } else { + resourceDir = selfPath.Parent().Parent().Join("res"); + } + + struct stat st; + if(stat(resourceDir.raw.c_str(), &st) != -1) { + // An executable-adjacent resource directory exists, good. + return resourceDir; + } + + // No executable-adjacent resource directory; use the one from compile-time prefix. + return Path::From(UNIX_DATADIR); +} + +static Platform::Path ResourcePath(const std::string &name) { + static Platform::Path resourceDir; + if(resourceDir.IsEmpty()) { + resourceDir = FindLocalResourceDir(); + } + + return resourceDir.Join(Path::FromPortable(name)); +} + +#endif + +#if !defined(WIN32) + +const void *LoadResource(const std::string &name, size_t *size) { + static std::map cache; + + auto it = cache.find(name); + if(it == cache.end()) { + ssassert(ReadFile(ResourcePath(name), &cache[name]), "Cannot read resource"); + it = cache.find(name); + } + + const std::string &content = (*it).second; + *size = content.size(); + return (const void*)content.data(); +} + +#endif + +} } diff --git a/src/platform/platform.h b/src/platform/platform.h index 37688851..06ae19ff 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// Common platform-dependent functionality. +// Platform-dependent functionality. // // Copyright 2017 whitequark //----------------------------------------------------------------------------- @@ -9,6 +9,14 @@ namespace Platform { +// UTF-8 ⟷ UTF-16 conversion, for Windows. +#if defined(WIN32) +std::string Narrow(const wchar_t *s); +std::wstring Widen(const char *s); +std::string Narrow(const std::wstring &s); +std::wstring Widen(const std::string &s); +#endif + // A filesystem path, respecting the conventions of the current platform. // Transformation functions return an empty path on error. class Path { @@ -46,6 +54,15 @@ struct PathLess { bool operator()(const Path &a, const Path &b) const { return a.raw < b.raw; } }; +// File manipulation functions. +FILE *OpenFile(const Platform::Path &filename, const char *mode); +bool ReadFile(const Platform::Path &filename, std::string *data); +bool WriteFile(const Platform::Path &filename, const std::string &data); +void RemoveFile(const Platform::Path &filename); + +// Resource loading function. +const void *LoadResource(const std::string &name, size_t *size); + } #endif diff --git a/src/platform/unixutil.cpp b/src/platform/unixutil.cpp index cae45953..125623c0 100644 --- a/src/platform/unixutil.cpp +++ b/src/platform/unixutil.cpp @@ -7,18 +7,8 @@ // Copyright 2008-2013 Jonathan Westhues. // Copyright 2013 Daniel Richard G. //----------------------------------------------------------------------------- -#include -#include #include -#ifdef __APPLE__ -# include // for strcasecmp -# include -# include -# include -#endif - #include "solvespace.h" -#include "config.h" namespace SolveSpace { @@ -59,143 +49,6 @@ void assert_failure(const char *file, unsigned line, const char *function, abort(); } -bool PathEqual(const std::string &a, const std::string &b) -{ -#if defined(__APPLE__) - // Case-sensitivity is actually per-volume on OS X, - // but it is tedious to implement and test for little benefit. - return !strcasecmp(a.c_str(), b.c_str()); -#else - return a == b; -#endif -} - -std::string PathSepPlatformToUnix(const std::string &filename) -{ - return filename; -} - -std::string PathSepUnixToPlatform(const std::string &filename) -{ - return filename; -} - -std::string PathFromCurrentDirectory(const std::string &relFilename) -{ - // On Unix we can just pass this to ssfopen directly. - return relFilename; -} - -FILE *ssfopen(const std::string &filename, const char *mode) -{ - ssassert(filename.length() == strlen(filename.c_str()), - "Unexpected null byte in middle of a path"); - return fopen(filename.c_str(), mode); -} - -void ssremove(const std::string &filename) -{ - ssassert(filename.length() == strlen(filename.c_str()), - "Unexpected null byte in middle of a path"); - remove(filename.c_str()); -} - -static std::string ExpandPath(std::string path) { - char *expanded_c_path = realpath(path.c_str(), NULL); - if(expanded_c_path == NULL) return ""; - - std::string expanded_path = expanded_c_path; - free(expanded_c_path); - return expanded_path; -} - -static const std::string &FindLocalResourceDir() { - static std::string resourceDir; - static bool checked; - - if(checked) return resourceDir; - checked = true; - - // Getting path to your own executable is a total portability disaster. - // Good job *nix OSes; you're basically all awful here. - std::string selfPath; -#if defined(__linux__) - selfPath = "/proc/self/exe"; -#elif defined(__NetBSD__) - selfPath = "/proc/curproc/exe" -#elif defined(__OpenBSD__) || defined(__FreeBSD__) - selfPath = "/proc/curproc/file"; -#elif defined(__APPLE__) - CFURLRef cfUrl = - CFBundleCopyExecutableURL(CFBundleGetMainBundle()); - CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle); - selfPath.resize(CFStringGetLength(cfPath) + 1); // reserve space for NUL - ssassert(CFStringGetCString(cfPath, &selfPath[0], selfPath.size(), kCFStringEncodingUTF8), - "Cannot convert CFString to C string"); - selfPath.resize(selfPath.size() - 1); - CFRelease(cfUrl); - CFRelease(cfPath); -#else - // We don't know how to find the local resource directory on this platform, - // so use the global one (by returning an empty string). - return resourceDir; -#endif - - resourceDir = ExpandPath(selfPath); - if(!resourceDir.empty()) { - resourceDir.erase(resourceDir.rfind('/')); - resourceDir += "/../res"; - resourceDir = ExpandPath(resourceDir); - } - if(!resourceDir.empty()) { - struct stat st; - if(stat(resourceDir.c_str(), &st)) { - // We looked at the path where the local resource directory ought to be, - // but there isn't one, so use the global one. - resourceDir = ""; - } - } - return resourceDir; -} - -const void *LoadResource(const std::string &name, size_t *size) { - static std::map cache; - - auto it = cache.find(name); - if(it == cache.end()) { - const std::string &resourceDir = FindLocalResourceDir(); - - std::string path; - if(resourceDir.empty()) { -#if defined(__APPLE__) - CFStringRef cfName = - CFStringCreateWithCString(kCFAllocatorDefault, name.c_str(), - kCFStringEncodingUTF8); - CFURLRef cfUrl = - CFBundleCopyResourceURL(CFBundleGetMainBundle(), cfName, NULL, NULL); - CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle); - path.resize(CFStringGetLength(cfPath) + 1); // reserve space for NUL - ssassert(CFStringGetCString(cfPath, &path[0], path.size(), kCFStringEncodingUTF8), - "Cannot convert CFString to C string"); - path.resize(path.size() - 1); - CFRelease(cfName); - CFRelease(cfUrl); - CFRelease(cfPath); -#else - path = (UNIX_DATADIR "/") + name; -#endif - } else { - path = resourceDir + "/" + name; - } - - ssassert(ReadFile(path, &cache[name]), "Cannot read resource"); - it = cache.find(name); - } - - *size = (*it).second.size(); - return static_cast(&(*it).second[0]); -} - //----------------------------------------------------------------------------- // A separate heap, on which we allocate expressions. Maybe a bit faster, // since fragmentation is less of a concern, and it also makes it possible diff --git a/src/platform/w32main.cpp b/src/platform/w32main.cpp index 9067a34f..ab031f3b 100644 --- a/src/platform/w32main.cpp +++ b/src/platform/w32main.cpp @@ -32,6 +32,9 @@ #include #endif +using Platform::Narrow; +using Platform::Widen; + HINSTANCE Instance; HWND TextWnd; @@ -443,9 +446,9 @@ static void ThawWindowPos(HWND hwnd, const std::string &name) ShowWindow(hwnd, SW_MAXIMIZE); } -void SolveSpace::SetCurrentFilename(const std::string &filename) { +void SolveSpace::SetCurrentFilename(const Platform::Path &filename) { SetWindowTextW(GraphicsWnd, - Title(filename.empty() ? C_("title", "(new sketch)") : filename).c_str()); + Title(filename.IsEmpty() ? C_("title", "(new sketch)") : filename.raw).c_str()); } void SolveSpace::SetMousePointerToHand(bool yes) { @@ -1091,7 +1094,7 @@ static std::string ConvertFilters(const FileFilter ssFilters[]) { return filter; } -static bool OpenSaveFile(bool isOpen, std::string *filename, const std::string &defExtension, +static bool OpenSaveFile(bool isOpen, Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]) { std::string activeExtension = defExtension; if(activeExtension == "") { @@ -1099,11 +1102,10 @@ static bool OpenSaveFile(bool isOpen, std::string *filename, const std::string & } std::wstring initialFilenameW; - if(filename->empty()) { + if(filename->IsEmpty()) { initialFilenameW = Widen("untitled"); } else { - initialFilenameW = Widen(Dirname(*filename) + PATH_SEP + - Basename(*filename, /*stripExtension=*/true)); + initialFilenameW = Widen(filename->Parent().Join(filename->FileStem()).raw); } std::wstring selPatternW = Widen(ConvertFilters(filters)); std::wstring defExtensionW = Widen(defExtension); @@ -1140,17 +1142,17 @@ static bool OpenSaveFile(bool isOpen, std::string *filename, const std::string & EnableWindow(GraphicsWnd, true); SetForegroundWindow(GraphicsWnd); - if(r) *filename = Narrow(filenameC); + if(r) *filename = Platform::Path::From(Narrow(filenameC)); return r ? true : false; } -bool SolveSpace::GetOpenFile(std::string *filename, const std::string &defExtension, +bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]) { return OpenSaveFile(/*isOpen=*/true, filename, defExtension, filters); } -bool SolveSpace::GetSaveFile(std::string *filename, const std::string &defExtension, +bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]) { return OpenSaveFile(/*isOpen=*/false, filename, defExtension, filters); @@ -1206,13 +1208,13 @@ DialogChoice SolveSpace::LoadAutosaveYesNo() } } -DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const std::string &filename, +DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const Platform::Path &filename, bool canCancel) { EnableWindow(GraphicsWnd, false); EnableWindow(TextWnd, false); std::string message = - "The linked file " + filename + " is not present.\n\n" + "The linked file " + filename.raw + " is not present.\n\n" "Do you want to locate it manually?\n\n" "If you select \"No\", any geometry that depends on " "the missing file will be removed."; @@ -1236,17 +1238,18 @@ DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const std::string &filena } } -std::vector SolveSpace::GetFontFiles() { - std::vector fonts; +std::vector SolveSpace::GetFontFiles() { + std::vector fonts; - std::wstring fontsDir(MAX_PATH, '\0'); - fontsDir.resize(GetWindowsDirectoryW(&fontsDir[0], fontsDir.length())); - fontsDir += L"\\fonts\\"; + std::wstring fontsDirW(MAX_PATH, '\0'); + fontsDirW.resize(GetWindowsDirectoryW(&fontsDirW[0], fontsDirW.length())); + fontsDirW += L"\\fonts\\"; + Platform::Path fontsDir = Platform::Path::From(Narrow(fontsDirW)); WIN32_FIND_DATA wfd; - HANDLE h = FindFirstFileW((fontsDir + L"*").c_str(), &wfd); + HANDLE h = FindFirstFileW((fontsDirW + L"*").c_str(), &wfd); while(h != INVALID_HANDLE_VALUE) { - fonts.push_back(Narrow(fontsDir) + Narrow(wfd.cFileName)); + fonts.push_back(fontsDir.Join(Narrow(wfd.cFileName))); if(!FindNextFileW(h, &wfd)) break; } @@ -1296,8 +1299,8 @@ static void DoRecent(HMENU m, Command base) ; int c = 0; for(size_t i = 0; i < MAX_RECENT; i++) { - if(!RecentFile[i].empty()) { - AppendMenuW(m, MF_STRING, (uint32_t)base + i, Widen(RecentFile[i]).c_str()); + if(!RecentFile[i].IsEmpty()) { + AppendMenuW(m, MF_STRING, (uint32_t)base + i, Widen(RecentFile[i].raw).c_str()); c++; } } @@ -1540,7 +1543,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, // A filename may have been specified on the command line; if so, then // strip any quotation marks, and make it absolute. if(args.size() >= 2) { - SS.OpenFile(PathFromCurrentDirectory(args[1])); + SS.Load(Platform::Path::From(args[1]).Expand(/*fromCurrentDirectory=*/true)); } // Repaint one more time, after we've set everything up. diff --git a/src/platform/w32util.cpp b/src/platform/w32util.cpp index 26fef6e9..4c4ae1ff 100644 --- a/src/platform/w32util.cpp +++ b/src/platform/w32util.cpp @@ -46,130 +46,6 @@ void assert_failure(const char *file, unsigned line, const char *function, #endif } -std::string Narrow(const wchar_t *in) -{ - std::string out; - DWORD len = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL); - out.resize(len - 1); - ssassert(WideCharToMultiByte(CP_UTF8, 0, in, -1, &out[0], len, NULL, NULL), - "Invalid UTF-16"); - return out; -} - -std::string Narrow(const std::wstring &in) -{ - if(in == L"") return ""; - - std::string out; - out.resize(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(), - NULL, 0, NULL, NULL)); - ssassert(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(), - &out[0], (int)out.length(), NULL, NULL), - "Invalid UTF-16"); - return out; -} - -std::wstring Widen(const char *in) -{ - std::wstring out; - DWORD len = MultiByteToWideChar(CP_UTF8, 0, in, -1, NULL, 0); - out.resize(len - 1); - ssassert(MultiByteToWideChar(CP_UTF8, 0, in, -1, &out[0], len), - "Invalid UTF-8"); - return out; -} - -std::wstring Widen(const std::string &in) -{ - if(in == "") return L""; - - std::wstring out; - out.resize(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), NULL, 0)); - ssassert(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), - &out[0], (int)out.length()), - "Invalid UTF-8"); - return out; -} - -bool PathEqual(const std::string &a, const std::string &b) -{ - // Case-sensitivity is actually per-volume on Windows, - // but it is tedious to implement and test for little benefit. - std::wstring wa = Widen(a), wb = Widen(b); - return std::equal(wa.begin(), wa.end(), wb.begin(), /*wb.end(),*/ - [](wchar_t wca, wchar_t wcb) { return towlower(wca) == towlower(wcb); }); - -} - -std::string PathSepPlatformToUnix(const std::string &filename) -{ - std::string result = filename; - for(size_t i = 0; i < result.length(); i++) { - if(result[i] == '\\') - result[i] = '/'; - } - return result; -} - -std::string PathSepUnixToPlatform(const std::string &filename) -{ - std::string result = filename; - for(size_t i = 0; i < result.length(); i++) { - if(result[i] == '/') - result[i] = '\\'; - } - return result; -} - -std::string PathFromCurrentDirectory(const std::string &relFilename) -{ - // On Windows, ssfopen needs an absolute UNC path proper, so get that. - std::wstring relFilenameW = Widen(relFilename); - std::wstring absFilenameW; - absFilenameW.resize(GetFullPathNameW(relFilenameW.c_str(), 0, NULL, NULL)); - DWORD length = GetFullPathNameW(relFilenameW.c_str(), (int)absFilenameW.length(), - &absFilenameW[0], NULL); - ssassert(length != 0, "Expected GetFullPathName to succeed"); - absFilenameW.resize(length); - return Narrow(absFilenameW); -} - -static std::string MakeUNCFilename(const std::string &filename) -{ - // Prepend \\?\ UNC prefix unless already an UNC path. - // We never try to fopen paths that are not absolute or - // contain separators inappropriate for the platform; - // thus, it is always safe to prepend this prefix. - std::string uncFilename = filename; - if(uncFilename.substr(0, 2) != "\\\\") - uncFilename = "\\\\?\\" + uncFilename; - return uncFilename; -} - -FILE *ssfopen(const std::string &filename, const char *mode) -{ - ssassert(filename.length() == strlen(filename.c_str()), - "Unexpected null byte in middle of a path"); - return _wfopen(Widen(MakeUNCFilename(filename)).c_str(), Widen(mode).c_str()); -} - -void ssremove(const std::string &filename) -{ - ssassert(filename.length() == strlen(filename.c_str()), - "Unexpected null byte in middle of a path"); - _wremove(Widen(filename).c_str()); -} - -const void *LoadResource(const std::string &name, size_t *size) { - HRSRC hres = FindResourceW(NULL, Widen(name).c_str(), RT_RCDATA); - ssassert(hres != NULL, "Cannot find resource"); - HGLOBAL res = ::LoadResource(NULL, hres); - ssassert(res != NULL, "Cannot load resource"); - - *size = SizeofResource(NULL, hres); - return LockResource(res); -} - //----------------------------------------------------------------------------- // A separate heap, on which we allocate expressions. Maybe a bit faster, // since no fragmentation issues whatsoever, and it also makes it possible @@ -231,7 +107,7 @@ std::vector InitPlatform(int argc, char **argv) { LPWSTR *argvW = CommandLineToArgvW(GetCommandLineW(), &argcW); std::vector args; for(int i = 0; i < argcW; i++) { - args.push_back(Narrow(argvW[i])); + args.push_back(Platform::Narrow(argvW[i])); } LocalFree(argvW); return args; diff --git a/src/render/gl2shader.cpp b/src/render/gl2shader.cpp index 5ce6b16d..062bc89a 100644 --- a/src/render/gl2shader.cpp +++ b/src/render/gl2shader.cpp @@ -58,7 +58,7 @@ Vector4f Vector4f::From(const RgbaColor &c) { static GLuint CompileShader(const std::string &res, GLenum type) { size_t size; - const char *resData = (const char *)LoadResource(res, &size); + const char *resData = (const char *)Platform::LoadResource(res, &size); // Sigh, here we go... We want to deploy to four platforms: Linux, Windows, OS X, mobile+web. // These platforms are basically disjunctive in the OpenGL versions and profiles that they diff --git a/src/resource.cpp b/src/resource.cpp index 1362db3e..c1501f39 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -16,7 +16,7 @@ namespace SolveSpace { std::string LoadString(const std::string &name) { size_t size; - const void *data = LoadResource(name, &size); + const void *data = Platform::LoadResource(name, &size); std::string result(static_cast(data), size); // When editing resources under Windows, Windows newlines may sneak in. @@ -30,7 +30,7 @@ std::string LoadString(const std::string &name) { std::string LoadStringFromGzip(const std::string &name) { size_t deflatedSize; - const void *data = LoadResource(name, &deflatedSize); + const void *data = Platform::LoadResource(name, &deflatedSize); z_stream stream; stream.zalloc = Z_NULL; @@ -62,7 +62,7 @@ std::string LoadStringFromGzip(const std::string &name) { std::shared_ptr LoadPng(const std::string &name) { size_t size; - const void *data = LoadResource(name, &size); + const void *data = Platform::LoadResource(name, &size); std::shared_ptr pixmap = Pixmap::FromPng(static_cast(data), size); ssassert(pixmap != nullptr, "Cannot load pixmap"); @@ -263,8 +263,8 @@ exit: return nullptr; } -std::shared_ptr Pixmap::ReadPng(const std::string &filename, bool flip) { - FILE *f = ssfopen(filename.c_str(), "rb"); +std::shared_ptr Pixmap::ReadPng(const Platform::Path &filename, bool flip) { + FILE *f = OpenFile(filename, "rb"); if(!f) return NULL; std::shared_ptr pixmap = ReadPng(f, flip); fclose(f); @@ -318,8 +318,8 @@ exit: return false; } -bool Pixmap::WritePng(const std::string &filename, bool flip) { - FILE *f = ssfopen(filename.c_str(), "wb"); +bool Pixmap::WritePng(const Platform::Path &filename, bool flip) { + FILE *f = OpenFile(filename, "wb"); if(!f) return false; bool success = WritePng(f, flip); fclose(f); diff --git a/src/resource.h b/src/resource.h index 858e3b2d..20bf6bd4 100644 --- a/src/resource.h +++ b/src/resource.h @@ -12,12 +12,6 @@ class Point2d; class Pixmap; class Vector; -// Only the following function is platform-specific. -// It returns a pointer to resource contents that is aligned to at least -// sizeof(void*) and has a global lifetime, or NULL if a resource with -// the specified name does not exist. -const void *LoadResource(const std::string &name, size_t *size); - std::string LoadString(const std::string &name); std::string LoadStringFromGzip(const std::string &name); std::shared_ptr LoadPng(const std::string &name); @@ -36,9 +30,9 @@ public: static std::shared_ptr FromPng(const uint8_t *data, size_t size, bool flip = false); static std::shared_ptr ReadPng(FILE *f, bool flip = false); - static std::shared_ptr ReadPng(const std::string &filename, bool flip = false); + static std::shared_ptr ReadPng(const Platform::Path &filename, bool flip = false); bool WritePng(FILE *f, bool flip = false); - bool WritePng(const std::string &filename, bool flip = false); + bool WritePng(const Platform::Path &filename, bool flip = false); size_t GetBytesPerPixel() const; RgbaColor GetPixel(size_t x, size_t y) const; diff --git a/src/sketch.h b/src/sketch.h index 492da2ef..35c26023 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -218,8 +218,7 @@ public: enum { REMAP_PRIME = 19477 }; int remapCache[REMAP_PRIME]; - std::string linkFile; - std::string linkFileRel; + Platform::Path linkFile; SMesh impMesh; SShell impShell; EntityList impEntity; diff --git a/src/solvespace.cpp b/src/solvespace.cpp index f18c1be9..67f76341 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -10,7 +10,7 @@ SolveSpaceUI SolveSpace::SS = {}; Sketch SolveSpace::SK = {}; -std::string SolveSpace::RecentFile[MAX_RECENT] = {}; +Platform::Path SolveSpace::RecentFile[MAX_RECENT] = {}; void SolveSpaceUI::Init() { #if !defined(HEADLESS) @@ -97,7 +97,7 @@ void SolveSpaceUI::Init() { showToolbar = CnfThawBool(true, "ShowToolbar"); // Recent files menus for(size_t i = 0; i < MAX_RECENT; i++) { - RecentFile[i] = CnfThawString("", "RecentFile_" + std::to_string(i)); + RecentFile[i] = Platform::Path::From(CnfThawString("", "RecentFile_" + std::to_string(i))); } RefreshRecentMenus(); // Autosave timer @@ -118,10 +118,10 @@ void SolveSpaceUI::Init() { AfterNewFile(); } -bool SolveSpaceUI::LoadAutosaveFor(const std::string &filename) { - std::string autosaveFile = filename + AUTOSAVE_SUFFIX; +bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) { + Platform::Path autosaveFile = filename.WithExtension(AUTOSAVE_EXT); - FILE *f = ssfopen(autosaveFile, "rb"); + FILE *f = OpenFile(autosaveFile, "rb"); if(!f) return false; fclose(f); @@ -134,14 +134,14 @@ bool SolveSpaceUI::LoadAutosaveFor(const std::string &filename) { return false; } -bool SolveSpaceUI::OpenFile(const std::string &filename) { +bool SolveSpaceUI::Load(const Platform::Path &filename) { bool autosaveLoaded = LoadAutosaveFor(filename); bool fileLoaded = autosaveLoaded || LoadFromFile(filename, /*canCancel=*/true); if(fileLoaded) { saveFile = filename; AddToRecentList(filename); } else { - saveFile = ""; + saveFile.Clear(); NewFile(); } AfterNewFile(); @@ -152,7 +152,7 @@ bool SolveSpaceUI::OpenFile(const std::string &filename) { void SolveSpaceUI::Exit() { // Recent files for(size_t i = 0; i < MAX_RECENT; i++) - CnfFreezeString(RecentFile[i], "RecentFile_" + std::to_string(i)); + CnfFreezeString(RecentFile[i].raw, "RecentFile_" + std::to_string(i)); // Model colors for(size_t i = 0; i < MODEL_COLORS; i++) CnfFreezeColor(modelColor[i], "ModelColor_" + std::to_string(i)); @@ -342,18 +342,18 @@ void SolveSpaceUI::AfterNewFile() { UpdateWindowTitle(); } -void SolveSpaceUI::RemoveFromRecentList(const std::string &filename) { +void SolveSpaceUI::RemoveFromRecentList(const Platform::Path &filename) { int dest = 0; for(int src = 0; src < (int)MAX_RECENT; src++) { - if(filename != RecentFile[src]) { + if(!filename.Equals(RecentFile[src])) { if(src != dest) RecentFile[dest] = RecentFile[src]; dest++; } } - while(dest < (int)MAX_RECENT) RecentFile[dest++].clear(); + while(dest < (int)MAX_RECENT) RecentFile[dest++].Clear(); RefreshRecentMenus(); } -void SolveSpaceUI::AddToRecentList(const std::string &filename) { +void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) { RemoveFromRecentList(filename); for(int src = MAX_RECENT - 2; src >= 0; src--) { @@ -364,22 +364,19 @@ void SolveSpaceUI::AddToRecentList(const std::string &filename) { } bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) { - std::string prevSaveFile = saveFile; + Platform::Path newSaveFile = saveFile; - if(saveAs || saveFile.empty()) { - if(!GetSaveFile(&saveFile, "", SlvsFileFilter)) return false; - // need to get new filename directly into saveFile, since that - // determines linkFileRel path + if(saveAs || saveFile.IsEmpty()) { + if(!GetSaveFile(&newSaveFile, "", SlvsFileFilter)) return false; } - if(SaveToFile(saveFile)) { - AddToRecentList(saveFile); + if(SaveToFile(newSaveFile)) { + AddToRecentList(newSaveFile); RemoveAutosave(); + saveFile = newSaveFile; unsaved = false; return true; } else { - // don't store an invalid save filename - saveFile = prevSaveFile; return false; } } @@ -388,16 +385,16 @@ bool SolveSpaceUI::Autosave() { SetAutosaveTimerFor(autosaveInterval); - if(!saveFile.empty() && unsaved) - return SaveToFile(saveFile + AUTOSAVE_SUFFIX); + if(!saveFile.IsEmpty() && unsaved) + return SaveToFile(saveFile.WithExtension(AUTOSAVE_EXT)); return false; } void SolveSpaceUI::RemoveAutosave() { - std::string autosaveFile = saveFile + AUTOSAVE_SUFFIX; - ssremove(autosaveFile); + Platform::Path autosaveFile = saveFile.WithExtension(AUTOSAVE_EXT); + RemoveFile(autosaveFile); } bool SolveSpaceUI::OkayToStartNewFile() { @@ -426,8 +423,8 @@ void SolveSpaceUI::MenuFile(Command id) { (uint32_t)id < ((uint32_t)Command::RECENT_OPEN+MAX_RECENT)) { if(!SS.OkayToStartNewFile()) return; - std::string newFile = RecentFile[(uint32_t)id - (uint32_t)Command::RECENT_OPEN]; - SS.OpenFile(newFile); + Platform::Path newFile = RecentFile[(uint32_t)id - (uint32_t)Command::RECENT_OPEN]; + SS.Load(newFile); return; } @@ -435,7 +432,7 @@ void SolveSpaceUI::MenuFile(Command id) { case Command::NEW: if(!SS.OkayToStartNewFile()) break; - SS.saveFile = ""; + SS.saveFile.Clear(); SS.NewFile(); SS.AfterNewFile(); break; @@ -443,9 +440,9 @@ void SolveSpaceUI::MenuFile(Command id) { case Command::OPEN: { if(!SS.OkayToStartNewFile()) break; - std::string newFile; + Platform::Path newFile; if(GetOpenFile(&newFile, "", SlvsFileFilter)) { - SS.OpenFile(newFile); + SS.Load(newFile); } break; } @@ -459,22 +456,22 @@ void SolveSpaceUI::MenuFile(Command id) { break; case Command::EXPORT_PNG: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(!GetSaveFile(&exportFile, "", PngFileFilter)) break; SS.ExportAsPngTo(exportFile); break; } case Command::EXPORT_VIEW: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(!GetSaveFile(&exportFile, CnfThawString("", "ViewExportFormat"), VectorFileFilter)) break; - CnfFreezeString(Extension(exportFile), "ViewExportFormat"); + CnfFreezeString(exportFile.Extension(), "ViewExportFormat"); // If the user is exporting something where it would be // inappropriate to include the constraints, then warn. if(SS.GW.showConstraints && - (FilenameHasExtension(exportFile, ".txt") || + (exportFile.HasExtension("txt") || fabs(SS.exportOffset) > LENGTH_EPS)) { Message(_("Constraints are currently shown, and will be exported " @@ -488,40 +485,40 @@ void SolveSpaceUI::MenuFile(Command id) { } case Command::EXPORT_WIREFRAME: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(!GetSaveFile(&exportFile, CnfThawString("", "WireframeExportFormat"), Vector3dFileFilter)) break; - CnfFreezeString(Extension(exportFile), "WireframeExportFormat"); + CnfFreezeString(exportFile.Extension(), "WireframeExportFormat"); SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/true); break; } case Command::EXPORT_SECTION: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(!GetSaveFile(&exportFile, CnfThawString("", "SectionExportFormat"), VectorFileFilter)) break; - CnfFreezeString(Extension(exportFile), "SectionExportFormat"); + CnfFreezeString(exportFile.Extension(), "SectionExportFormat"); SS.ExportSectionTo(exportFile); break; } case Command::EXPORT_MESH: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(!GetSaveFile(&exportFile, CnfThawString("", "MeshExportFormat"), MeshFileFilter)) break; - CnfFreezeString(Extension(exportFile), "MeshExportFormat"); + CnfFreezeString(exportFile.Extension(), "MeshExportFormat"); SS.ExportMeshTo(exportFile); break; } case Command::EXPORT_SURFACES: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(!GetSaveFile(&exportFile, CnfThawString("", "SurfacesExportFormat"), SurfaceFileFilter)) break; - CnfFreezeString(Extension(exportFile), "SurfacesExportFormat"); + CnfFreezeString(exportFile.Extension(), "SurfacesExportFormat"); StepFileWriter sfw = {}; sfw.ExportSurfacesTo(exportFile); @@ -529,18 +526,18 @@ void SolveSpaceUI::MenuFile(Command id) { } case Command::IMPORT: { - std::string importFile; + Platform::Path importFile; if(!GetOpenFile(&importFile, CnfThawString("", "ImportFormat"), ImportableFileFilter)) break; - CnfFreezeString(Extension(importFile), "ImportFormat"); + CnfFreezeString(importFile.Extension(), "ImportFormat"); - if(Extension(importFile) == "dxf") { + if(importFile.HasExtension("dxf")) { ImportDxf(importFile); - } else if(Extension(importFile) == "dwg") { + } else if(importFile.HasExtension("dwg")) { ImportDwg(importFile); } else { Error("Can't identify file type from file extension of " - "filename '%s'; try .dxf or .dwg.", importFile.c_str()); + "filename '%s'; try .dxf or .dwg.", importFile.raw.c_str()); } SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); @@ -760,9 +757,9 @@ void SolveSpaceUI::MenuAnalyze(Command id) { break; case Command::STOP_TRACING: { - std::string exportFile = SS.saveFile; + Platform::Path exportFile = SS.saveFile; if(GetSaveFile(&exportFile, "", CsvFileFilter)) { - FILE *f = ssfopen(exportFile, "wb"); + FILE *f = OpenFile(exportFile, "wb"); if(f) { int i; SContour *sc = &(SS.traced.path); @@ -774,7 +771,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { } fclose(f); } else { - Error("Couldn't write to '%s'", exportFile.c_str()); + Error("Couldn't write to '%s'", exportFile.raw.c_str()); } } // Clear the trace, and stop tracing diff --git a/src/solvespace.h b/src/solvespace.h index 33d93817..77181913 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -121,13 +121,6 @@ inline double ffabs(double v) { return (v > 0) ? v : (-v); } #define VERY_POSITIVE (1e10) #define VERY_NEGATIVE (-1e10) -#if defined(WIN32) -std::string Narrow(const wchar_t *s); -std::wstring Widen(const char *s); -std::string Narrow(const std::wstring &s); -std::wstring Widen(const std::string &s); -#endif - inline double Random(double vmax) { return (vmax*rand()) / RAND_MAX; } @@ -144,30 +137,17 @@ enum class ContextCommand : uint32_t; #include "platform/platform.h" -#if defined(WIN32) -#define PATH_SEP "\\" -#else -#define PATH_SEP "/" -#endif - -bool PathEqual(const std::string &a, const std::string &b); -std::string PathSepPlatformToUnix(const std::string &filename); -std::string PathSepUnixToPlatform(const std::string &filename); -std::string PathFromCurrentDirectory(const std::string &relFilename); -FILE *ssfopen(const std::string &filename, const char *mode); -void ssremove(const std::string &filename); - const size_t MAX_RECENT = 8; -extern std::string RecentFile[MAX_RECENT]; +extern Platform::Path RecentFile[MAX_RECENT]; void RefreshRecentMenus(); enum DialogChoice { DIALOG_YES = 1, DIALOG_NO = -1, DIALOG_CANCEL = 0 }; DialogChoice SaveFileYesNoCancel(); DialogChoice LoadAutosaveYesNo(); -DialogChoice LocateImportedFileYesNoCancel(const std::string &filename, +DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, bool canCancel); -#define AUTOSAVE_SUFFIX "~" +#define AUTOSAVE_EXT "slvs~" enum class Unit : uint32_t { MM = 0, @@ -176,11 +156,11 @@ enum class Unit : uint32_t { struct FileFilter; -bool GetSaveFile(std::string *filename, const std::string &defExtension, +bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]); -bool GetOpenFile(std::string *filename, const std::string &defExtension, +bool GetOpenFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]); -std::vector GetFontFiles(); +std::vector GetFontFiles(); void OpenWebsite(const char *url); @@ -219,7 +199,7 @@ void dbp(const char *str, ...); dbp("tri: (%.3f %.3f %.3f) (%.3f %.3f %.3f) (%.3f %.3f %.3f)", \ CO((tri).a), CO((tri).b), CO((tri).c)) -void SetCurrentFilename(const std::string &filename); +void SetCurrentFilename(const Platform::Path &filename); void SetMousePointerToHand(bool yes); void DoMessageBox(const char *str, int rows, int cols, bool error); void SetTimerFor(int milliseconds); @@ -314,12 +294,6 @@ void MakeMatrix(double *mat, double a11, double a12, double a13, double a14, void MultMatrix(double *mata, double *matb, double *matr); std::string MakeAcceleratorLabel(int accel); -bool FilenameHasExtension(const std::string &str, const char *ext); -std::string Extension(const std::string &filename); -std::string Basename(std::string filename, bool stripExtension = false); -std::string Dirname(std::string filename); -bool ReadFile(const std::string &filename, std::string *data); -bool WriteFile(const std::string &filename, const std::string &data); void Message(const char *str, ...); void Error(const char *str, ...); void CnfFreezeBool(bool v, const std::string &name); @@ -411,7 +385,7 @@ public: class StepFileWriter { public: - void ExportSurfacesTo(const std::string &filename); + void ExportSurfacesTo(const Platform::Path &filename); void WriteHeader(); void WriteProductHeader(); int ExportCurve(SBezier *sb); @@ -433,12 +407,12 @@ protected: public: FILE *f; - std::string filename; + Platform::Path filename; Vector ptMin, ptMax; static double MmToPts(double mm); - static VectorFileWriter *ForFile(const std::string &filename); + static VectorFileWriter *ForFile(const Platform::Path &filename); void SetModelviewProjection(const Vector &u, const Vector &v, const Vector &n, const Vector &origin, double cameraTan, double scale); @@ -674,7 +648,7 @@ public: bool drawBackFaces; bool checkClosedContour; bool showToolbar; - std::string screenshotFile; + Platform::Path screenshotFile; RgbaColor backgroundColor; bool exportShadedTriangles; bool exportPwlCurves; @@ -725,16 +699,16 @@ public: // The platform-dependent code calls this before entering the msg loop void Init(); - bool OpenFile(const std::string &filename); + bool Load(const Platform::Path &filename); void Exit(); // File load/save routines, including the additional files that get // loaded when we have link groups. FILE *fh; void AfterNewFile(); - static void RemoveFromRecentList(const std::string &filename); - static void AddToRecentList(const std::string &filename); - std::string saveFile; + static void RemoveFromRecentList(const Platform::Path &filename); + static void AddToRecentList(const Platform::Path &filename); + Platform::Path saveFile; bool fileLoadError; bool unsaved; typedef struct { @@ -744,8 +718,8 @@ public: void *ptr; } SaveTable; static const SaveTable SAVED[]; - void SaveUsingTable(int type); - void LoadUsingTable(char *key, char *val); + void SaveUsingTable(const Platform::Path &filename, int type); + void LoadUsingTable(const Platform::Path &filename, char *key, char *val); struct { Group g; Request r; @@ -763,22 +737,22 @@ public: void UpdateWindowTitle(); void ClearExisting(); void NewFile(); - bool SaveToFile(const std::string &filename); - bool LoadAutosaveFor(const std::string &filename); - bool LoadFromFile(const std::string &filename, bool canCancel = false); + bool SaveToFile(const Platform::Path &filename); + bool LoadAutosaveFor(const Platform::Path &filename); + bool LoadFromFile(const Platform::Path &filename, bool canCancel = false); void UpgradeLegacyData(); - bool LoadEntitiesFromFile(const std::string &filename, EntityList *le, + bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh); - bool ReloadAllImported(const std::string &filename = "", bool canCancel = false); + bool ReloadAllImported(const Platform::Path &filename, bool canCancel = false); // And the various export options - void ExportAsPngTo(const std::string &filename); - void ExportMeshTo(const std::string &filename); + void ExportAsPngTo(const Platform::Path &filename); + void ExportMeshTo(const Platform::Path &filename); void ExportMeshAsStlTo(FILE *f, SMesh *sm); void ExportMeshAsObjTo(FILE *fObj, FILE *fMtl, SMesh *sm); - void ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, + void ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename, SMesh *sm, SOutlineList *sol); - void ExportViewOrWireframeTo(const std::string &filename, bool exportWireframe); - void ExportSectionTo(const std::string &filename); + void ExportViewOrWireframeTo(const Platform::Path &filename, bool exportWireframe); + void ExportSectionTo(const Platform::Path &filename); void ExportWireframeCurves(SEdgeList *sel, SBezierList *sbl, VectorFileWriter *out); void ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm, @@ -903,8 +877,8 @@ public: } }; -void ImportDxf(const std::string &file); -void ImportDwg(const std::string &file); +void ImportDxf(const Platform::Path &file); +void ImportDwg(const Platform::Path &file); extern SolveSpaceUI SS; extern Sketch SK; diff --git a/src/style.cpp b/src/style.cpp index 4af11c87..eb30516f 100644 --- a/src/style.cpp +++ b/src/style.cpp @@ -393,16 +393,16 @@ void TextWindow::ScreenBackgroundImage(int link, uint32_t v) { SS.bgImage.pixmap = nullptr; if(link == 'l') { - std::string bgImageFile; + Platform::Path bgImageFile; if(GetOpenFile(&bgImageFile, "", PngFileFilter)) { - FILE *f = ssfopen(bgImageFile, "rb"); + FILE *f = OpenFile(bgImageFile, "rb"); if(f) { SS.bgImage.pixmap = Pixmap::ReadPng(f); SS.bgImage.scale = SS.GW.scale; SS.bgImage.origin = SS.GW.offset.ScaledBy(-1); fclose(f); } else { - Error("Error reading PNG file '%s'", bgImageFile.c_str()); + Error("Error reading PNG file '%s'", bgImageFile.raw.c_str()); } } } diff --git a/src/textscreens.cpp b/src/textscreens.cpp index 8b3eb48e..49ba0ac8 100644 --- a/src/textscreens.cpp +++ b/src/textscreens.cpp @@ -327,7 +327,12 @@ void TextWindow::ShowGroupInfo() { } } else if(g->type == Group::Type::LINKED) { Printf(true, " %Ftlink geometry from file%E"); - Printf(false, "%Ba '%s'", g->linkFileRel.c_str()); + Platform::Path relativePath =g->linkFile.RelativeTo(SS.saveFile.Parent()); + if(relativePath.IsEmpty()) { + Printf(false, "%Ba '%s'", g->linkFile.raw.c_str()); + } else { + Printf(false, "%Ba '%s'", relativePath.raw.c_str()); + } Printf(false, "%Bd %Ftscaled by%E %# %Fl%Ll%f%D[change]%E", g->scale, &TextWindow::ScreenChangeGroupScale, g->h.v); diff --git a/src/ttf.cpp b/src/ttf.cpp index 5d644675..8251a1d5 100644 --- a/src/ttf.cpp +++ b/src/ttf.cpp @@ -56,7 +56,7 @@ TtfFontList::~TtfFontList() { void TtfFontList::LoadAll() { if(loaded) return; - for(const std::string &font : GetFontFiles()) { + for(const Platform::Path &font : GetFontFiles()) { TtfFont tf = {}; tf.fontFile = font; if(tf.LoadFromFile(fontLibrary)) @@ -127,7 +127,7 @@ double TtfFontList::AspectRatio(const std::string &font, const std::string &str) // entities that reference us will store it. //----------------------------------------------------------------------------- std::string TtfFont::FontFileBaseName() const { - return Basename(fontFile); + return fontFile.FileName(); } //----------------------------------------------------------------------------- @@ -138,20 +138,20 @@ std::string TtfFont::FontFileBaseName() const { bool TtfFont::LoadFromFile(FT_Library fontLibrary, bool nameOnly) { FT_Open_Args args = {}; args.flags = FT_OPEN_PATHNAME; - args.pathname = &fontFile[0]; // FT_String is char* for historical reasons + args.pathname = &fontFile.raw[0]; // FT_String is char* for historical reasons - // We don't use ssfopen() here to let freetype do its own memory management. + // We don't use OpenFile() here to let freetype do its own memory management. // This is OK because on Linux/OS X we just delegate to fopen and on Windows // we only look into C:\Windows\Fonts, which has a known short path. if(int fterr = FT_Open_Face(fontLibrary, &args, 0, &fontFace)) { dbp("freetype: loading font from file '%s' failed: %s", - fontFile.c_str(), ft_error_string(fterr)); + fontFile.raw.c_str(), ft_error_string(fterr)); return false; } if(int fterr = FT_Select_Charmap(fontFace, FT_ENCODING_UNICODE)) { dbp("freetype: loading unicode CMap for file '%s' failed: %s", - fontFile.c_str(), ft_error_string(fterr)); + fontFile.raw.c_str(), ft_error_string(fterr)); FT_Done_Face(fontFace); fontFace = NULL; return false; diff --git a/src/ttf.h b/src/ttf.h index ff016c39..fe82fee9 100644 --- a/src/ttf.h +++ b/src/ttf.h @@ -11,7 +11,7 @@ class TtfFont { public: - std::string fontFile; + Platform::Path fontFile; std::string name; FT_FaceRec_ *fontFace; double capHeight; diff --git a/src/undoredo.cpp b/src/undoredo.cpp index 92fbb983..be1fef2f 100644 --- a/src/undoredo.cpp +++ b/src/undoredo.cpp @@ -138,7 +138,7 @@ void SolveSpaceUI::PopOntoCurrentFrom(UndoStack *uk) { // sketch just changed a lot. SS.GW.ClearSuper(); SS.TW.ClearSuper(); - SS.ReloadAllImported(); + SS.ReloadAllImported(SS.saveFile); SS.GenerateAll(SolveSpaceUI::Generate::ALL); SS.ScheduleShowTW(); diff --git a/src/util.cpp b/src/util.cpp index b1d179ab..ecf0eb01 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -48,83 +48,6 @@ char32_t utf8_iterator::operator*() return result; } -bool SolveSpace::FilenameHasExtension(const std::string &str, const char *ext) -{ - int i, ls = str.length(), le = strlen(ext); - - if(ls < le) return false; - - for(i = 0; i < le; i++) { - if(tolower(ext[le-i-1]) != tolower(str[ls-i-1])) { - return false; - } - } - return true; -} - -std::string SolveSpace::Extension(const std::string &filename) { - int dot = filename.rfind('.'); - if(dot >= 0) { - std::string ext = filename.substr(dot + 1, filename.length()); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - return ext; - } - - return ""; -} - -std::string SolveSpace::Basename(std::string filename, bool stripExtension) { - int slash = filename.rfind(PATH_SEP); - if(slash >= 0) { - filename = filename.substr(slash + 1, filename.length()); - } - - if(stripExtension) { - int dot = filename.rfind('.'); - if(dot >= 0) { - filename = filename.substr(0, dot); - } - } - - return filename; -} - -std::string SolveSpace::Dirname(std::string filename) { - int slash = filename.rfind(PATH_SEP); - if(slash >= 0) { - return filename.substr(0, slash); - } - - return ""; -} - -bool SolveSpace::ReadFile(const std::string &filename, std::string *data) -{ - FILE *f = ssfopen(filename.c_str(), "rb"); - if(f == NULL) - return false; - - fseek(f, 0, SEEK_END); - data->resize(ftell(f)); - fseek(f, 0, SEEK_SET); - fread(&(*data)[0], 1, data->size(), f); - fclose(f); - - return true; -} - -bool SolveSpace::WriteFile(const std::string &filename, const std::string &data) -{ - FILE *f = ssfopen(filename.c_str(), "wb"); - if(f == NULL) - return false; - - fwrite(&data[0], 1, data.size(), f); - fclose(f); - - return true; -} - int64_t SolveSpace::GetMilliseconds() { auto timestamp = std::chrono::steady_clock::now().time_since_epoch(); diff --git a/test/harness.cpp b/test/harness.cpp index 79465c7a..70f340c6 100644 --- a/test/harness.cpp +++ b/test/harness.cpp @@ -16,7 +16,7 @@ namespace SolveSpace { // These are defined in headless.cpp, and aren't exposed in solvespace.h. - extern std::vector fontFiles; + extern std::vector fontFiles; extern bool antialias; extern std::shared_ptr framebuffer; } @@ -24,7 +24,6 @@ namespace SolveSpace { // The paths in __FILE__ are from the build system, but defined(WIN32) returns // the value for the host system. #define BUILD_PATH_SEP (__FILE__[0]=='/' ? '/' : '\\') -#define HOST_PATH_SEP PATH_SEP static std::string BuildRoot() { static std::string rootDir; @@ -35,40 +34,24 @@ static std::string BuildRoot() { return rootDir; } -static std::string HostRoot() { - static std::string rootDir; - if(!rootDir.empty()) return rootDir; +static Platform::Path HostRoot() { + static Platform::Path rootDir; + if(!rootDir.IsEmpty()) return rootDir; // No especially good way to do this, so let's assume somewhere up from // the current directory there's our repository, with CMakeLists.txt, and // pivot from there. -#if defined(WIN32) - wchar_t currentDirW[MAX_PATH]; - GetCurrentDirectoryW(MAX_PATH, currentDirW); - rootDir = Narrow(currentDirW); -#else - rootDir = "."; -#endif + rootDir = Platform::Path::CurrentDirectory(); // We're never more than four levels deep. for(size_t i = 0; i < 4; i++) { - std::string listsPath = rootDir; - listsPath += HOST_PATH_SEP; - listsPath += "CMakeLists.txt"; - FILE *f = ssfopen(listsPath, "r"); + FILE *f = OpenFile(rootDir.Join("CMakeLists.txt"), "r"); if(f) { fclose(f); - rootDir += HOST_PATH_SEP; - rootDir += "test"; + rootDir = rootDir.Join("test"); return rootDir; } - - if(rootDir[0] == '.') { - rootDir += HOST_PATH_SEP; - rootDir += ".."; - } else { - rootDir.erase(rootDir.rfind(HOST_PATH_SEP)); - } + rootDir = rootDir.Parent(); } ssassert(false, "Couldn't locate repository root"); @@ -164,14 +147,14 @@ void Test::Helper::PrintFailure(const char *file, int line, std::string msg) { BUILD_PATH_SEP, shortFile.c_str(), line, msg.c_str()); } -std::string Test::Helper::GetAssetPath(std::string testFile, std::string assetName, - std::string mangle) { +Platform::Path Test::Helper::GetAssetPath(std::string testFile, std::string assetName, + std::string mangle) { if(!mangle.empty()) { assetName.insert(assetName.rfind('.'), "." + mangle); } testFile.erase(0, BuildRoot().size()); testFile.erase(testFile.rfind(BUILD_PATH_SEP) + 1); - return PathSepUnixToPlatform(HostRoot() + "/" + testFile + assetName); + return HostRoot().Join(Platform::Path::FromPortable(testFile + assetName)); } bool Test::Helper::CheckBool(const char *file, int line, const char *expr, bool value, @@ -213,16 +196,16 @@ bool Test::Helper::CheckEqualEpsilon(const char *file, int line, const char *val } bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) { - std::string fixturePath = GetAssetPath(file, fixture); + Platform::Path fixturePath = GetAssetPath(file, fixture); - FILE *f = ssfopen(fixturePath.c_str(), "rb"); + FILE *f = OpenFile(fixturePath, "rb"); bool fixtureExists = (f != NULL); if(f) fclose(f); bool result = fixtureExists && SS.LoadFromFile(fixturePath); if(!RecordCheck(result)) { PrintFailure(file, line, - ssprintf("loading file '%s'", fixturePath.c_str())); + ssprintf("loading file '%s'", fixturePath.raw.c_str())); return false; } else { SS.AfterNewFile(); @@ -233,11 +216,11 @@ bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) { } bool Test::Helper::CheckSave(const char *file, int line, const char *reference) { - std::string refPath = GetAssetPath(file, reference), - outPath = GetAssetPath(file, reference, "out"); + Platform::Path refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"); if(!RecordCheck(SS.SaveToFile(outPath))) { PrintFailure(file, line, - ssprintf("saving file '%s'", refPath.c_str())); + ssprintf("saving file '%s'", refPath.raw.c_str())); return false; } else { std::string refData, outData; @@ -248,7 +231,7 @@ bool Test::Helper::CheckSave(const char *file, int line, const char *reference) return false; } - ssremove(outPath); + RemoveFile(outPath); return true; } } @@ -256,11 +239,11 @@ 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(); - std::string refPath = GetAssetPath(file, reference), - outPath = GetAssetPath(file, reference, "out"), - diffPath = GetAssetPath(file, reference, "diff"); + Platform::Path refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"), + diffPath = GetAssetPath(file, reference, "diff"); - std::shared_ptr refPixmap = Pixmap::ReadPng(refPath.c_str(), /*flip=*/true); + std::shared_ptr refPixmap = Pixmap::ReadPng(refPath, /*flip=*/true); if(!RecordCheck(refPixmap && refPixmap->Equals(*framebuffer))) { framebuffer->WritePng(outPath, /*flip=*/true); @@ -287,7 +270,7 @@ bool Test::Helper::CheckRender(const char *file, int line, const char *reference } } - diffPixmap->WritePng(diffPath.c_str(), /*flip=*/true); + diffPixmap->WritePng(diffPath, /*flip=*/true); std::string message = ssprintf("render doesn't match reference; %d (%.2f%%) pixels differ", diffPixelCount, @@ -296,8 +279,8 @@ bool Test::Helper::CheckRender(const char *file, int line, const char *reference } return false; } else { - ssremove(outPath); - ssremove(diffPath); + RemoveFile(outPath); + RemoveFile(diffPath); return true; } } @@ -336,7 +319,7 @@ int main(int argc, char **argv) { return 1; } - fontFiles.push_back(HostRoot() + HOST_PATH_SEP + "Gentium-R.ttf"); + fontFiles.push_back(HostRoot().Join("Gentium-R.ttf")); // Different Cairo versions have different antialiasing algorithms. antialias = false; diff --git a/test/harness.h b/test/harness.h index e129dbcc..fa959000 100644 --- a/test/harness.h +++ b/test/harness.h @@ -19,8 +19,8 @@ public: bool RecordCheck(bool success); void PrintFailure(const char *file, int line, std::string msg); - std::string GetAssetPath(std::string testFile, std::string assetName, - std::string mangle = ""); + Platform::Path GetAssetPath(std::string testFile, std::string assetName, + std::string mangle = ""); bool CheckBool(const char *file, int line, const char *expr, bool value, bool reference);