From f24751c0d9e42f6a91a1d2741a10d1b481e76a43 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 21 Aug 2023 23:15:16 -0400 Subject: [PATCH 1/7] Background: don't automatically Fill the starfield This allows using the Background::Container in non-Game contexts --- src/Background.cpp | 16 +++++++--------- src/Background.h | 6 ++++-- src/Intro.cpp | 3 ++- src/Space.cpp | 34 ++++++++++++++++++---------------- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/Background.cpp b/src/Background.cpp index 63b2e190198..1b9722c4e98 100644 --- a/src/Background.cpp +++ b/src/Background.cpp @@ -14,8 +14,8 @@ #include "core/TaskGraph.h" -#include "galaxy/StarSystem.h" #include "galaxy/GalaxyGenerator.h" +#include "galaxy/StarSystem.h" #include "graphics/Graphics.h" #include "graphics/RenderState.h" @@ -200,11 +200,10 @@ namespace Background { m_material->SetTexture("texture0"_hash, m_cubemap.Get()); } - Starfield::Starfield(Graphics::Renderer *renderer, Random &rand, const SystemPath *const systemPath, RefCountedPtr galaxy) + Starfield::Starfield(Graphics::Renderer *renderer) { m_renderer = renderer; Init(); - Fill(rand, systemPath, galaxy); } void Starfield::Init() @@ -262,7 +261,7 @@ namespace Background { class SampleStarTask : public Task { public: - SampleStarTask( RefCountedPtr galaxy, StarQueryInfo info, StarInfo* outStars, TaskRange range ) : + SampleStarTask(RefCountedPtr galaxy, StarQueryInfo info, StarInfo *outStars, TaskRange range) : Task(range), galaxy(galaxy), info(info), @@ -276,7 +275,7 @@ namespace Background { void SampleStars(TaskRange range) { PROFILE_SCOPED() - const SystemPath* systemPath = info.systemPath; + const SystemPath *systemPath = info.systemPath; int32_t minZ = info.sectorMin + int32_t(range.begin); int32_t maxZ = info.sectorMin + int32_t(range.end); @@ -325,7 +324,7 @@ namespace Background { //const Color col(Color::PINK); // debug pink // use a logarithmic scala for brightness since this looks more natural to the human eye - float brightness = log( luminositySystemSum / (4 * M_PI * distance.Length() * distance.Length()) ); + float brightness = log(luminositySystemSum / (4 * M_PI * distance.Length() * distance.Length())); stars.pos.push_back(distance.Normalized() * 1000.0f); stars.color.push_back(col); @@ -485,7 +484,6 @@ namespace Background { // star collection on the 'main' thread as well. auto handle = graph->QueueTaskSet(pickStarTaskSet); graph->WaitForTaskSet(handle); - } num = stars.pos.size(); Output("Stars picked from galaxy: %d\n", stars.pos.size()); @@ -660,10 +658,10 @@ namespace Background { m_renderer->DrawMesh(m_meshObject.get(), m_material.Get()); } - Container::Container(Graphics::Renderer *renderer, Random &rand, const Space *space, RefCountedPtr galaxy, const SystemPath *const systemPath) : + Container::Container(Graphics::Renderer *renderer, Random &rand) : m_renderer(renderer), m_milkyWay(renderer), - m_starField(renderer, rand, space && space->GetStarSystem() ? &space->GetStarSystem()->GetPath() : systemPath, galaxy), + m_starField(renderer), m_universeBox(renderer), m_drawFlags(DRAW_SKYBOX | DRAW_STARS) { diff --git a/src/Background.h b/src/Background.h index 27419da8390..5346ea997f1 100644 --- a/src/Background.h +++ b/src/Background.h @@ -61,7 +61,7 @@ namespace Background { class Starfield : public BackgroundElement { public: //does not Fill the starfield - Starfield(Graphics::Renderer *r, Random &rand, const SystemPath *const systemPath, RefCountedPtr galaxy); + Starfield(Graphics::Renderer *r); void Draw(); //create or recreate the starfield void Fill(Random &rand, const SystemPath *const systemPath, RefCountedPtr galaxy); @@ -95,13 +95,15 @@ namespace Background { DRAW_SKYBOX = 1 << 2 }; - Container(Graphics::Renderer *, Random &rand, const Space *space, RefCountedPtr galaxy, const SystemPath *const systemPath = nullptr); + Container(Graphics::Renderer *, Random &rand); void Draw(const matrix4x4d &transform); void SetIntensity(float intensity); void SetDrawFlags(const Uint32 flags); Uint32 GetDrawFlags() const { return m_drawFlags; } + Starfield *GetStarfield() { return &m_starField; } + private: Graphics::Renderer *m_renderer; MilkyWay m_milkyWay; diff --git a/src/Intro.cpp b/src/Intro.cpp index 5b9a8443744..2d2ed9e66c0 100644 --- a/src/Intro.cpp +++ b/src/Intro.cpp @@ -96,7 +96,8 @@ void Intro::RefreshBackground(Graphics::Renderer *r) { const SystemPath s(0, 0, 0); RefCountedPtr galaxy(GalaxyGenerator::Create()); - m_background.reset(new Background::Container(r, Pi::rng, nullptr, galaxy, &s)); + m_background.reset(new Background::Container(r, Pi::rng)); + m_background->GetStarfield()->Fill(Pi::rng, &s, galaxy); } void Intro::Reset() diff --git a/src/Space.cpp b/src/Space.cpp index 82d4ddcc1d7..2b10bf25834 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -202,7 +202,7 @@ Space::Space(Game *game, RefCountedPtr galaxy, Space *oldSpace) : m_processingFinalizationQueue(false) #endif { - m_background.reset(new Background::Container(Pi::renderer, Pi::rng, this, m_game->GetGalaxy())); + RefreshBackground(); m_rootFrameId = Frame::CreateFrame(FrameId::Invalid, Lang::SYSTEM, Frame::FLAG_DEFAULT, FLT_MAX); @@ -222,9 +222,7 @@ Space::Space(Game *game, RefCountedPtr galaxy, const SystemPath &path, S #endif { PROFILE_SCOPED() - Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED }; - Random rand(_init, 5); - m_background.reset(new Background::Container(Pi::renderer, rand, this, m_game->GetGalaxy())); + RefreshBackground(); CityOnPlanet::SetCityModelPatterns(m_starSystem->GetPath()); @@ -253,10 +251,7 @@ Space::Space(Game *game, RefCountedPtr galaxy, const Json &jsonObj, doub m_starSystem = StarSystem::FromJson(galaxy, spaceObj); - const SystemPath &path = m_starSystem->GetPath(); - Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED }; - Random rand(_init, 5); - m_background.reset(new Background::Container(Pi::renderer, rand, this, m_game->GetGalaxy())); + RefreshBackground(); RebuildSystemBodyIndex(); @@ -303,7 +298,7 @@ Space::Space(Game *game, RefCountedPtr galaxy, const Json &jsonObj, doub } } - GenSectorCache(galaxy, &path); + GenSectorCache(galaxy, &m_starSystem->GetPath()); //DebugDumpFrames(); } @@ -326,10 +321,16 @@ Space::~Space() void Space::RefreshBackground() { PROFILE_SCOPED() - const SystemPath &path = m_starSystem->GetPath(); - Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED }; - Random rand(_init, 5); - m_background.reset(new Background::Container(Pi::renderer, rand, this, m_game->GetGalaxy())); + if (m_starSystem.Valid()) { + const SystemPath &path = m_starSystem->GetPath(); + Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED }; + Random rand(_init, 5); + m_background.reset(new Background::Container(Pi::renderer, rand)); + m_background->GetStarfield()->Fill(rand, &this->GetStarSystem()->GetPath(), m_game->GetGalaxy()); + } else { + m_background.reset(new Background::Container(Pi::renderer, Pi::rng)); + m_background->GetStarfield()->Fill(Pi::rng, nullptr, m_game->GetGalaxy()); + } } void Space::ToJson(Json &jsonObj) @@ -555,13 +556,13 @@ std::vector Space::BodiesInAngle(const Body *b, const vector3d if (body->IsDead()) continue; //offset from the body center - like for view from ship cocpit - vector3d dirBody = body->GetPositionRelTo(b) * b->GetOrient() - offset; + vector3d dirBody = body->GetPositionRelTo(b) * b->GetOrient() - offset; double d = dirBody.Length(); //Normalizing but not using Normalized() function to avoid calculating Length again dirBody = dirBody / d; //Bodies outside of the cone disregarded - if(dirBody.Dot(view_dir) < cosOfMaxAngle) + if (dirBody.Dot(view_dir) < cosOfMaxAngle) continue; ret.emplace_back(body, d); @@ -620,7 +621,8 @@ class SectorDistanceSort { } SectorDistanceSort(const SystemPath *centre) : here(centre) - {} + { + } private: SectorDistanceSort() {} From 34270f6a26af0875a2e4ec88495b3082a911499e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 28 Aug 2023 21:14:19 -0400 Subject: [PATCH 2/7] ModManager: allow disabling mods - Mods are not loaded if there is a key in the configuration file specifying the mod as disabled. - Provide an interface to enumerate loaded mods, get their file paths, and access their filesystem --- src/FileSystem.h | 113 +++++++++++++++++++++------------------ src/ModManager.cpp | 66 +++++++++++++++++++---- src/ModManager.h | 25 +++++++++ src/Pi.cpp | 3 ++ src/editor/EditorApp.cpp | 4 ++ 5 files changed, 151 insertions(+), 60 deletions(-) diff --git a/src/FileSystem.h b/src/FileSystem.h index cc231a6c02c..5ff6fb50731 100644 --- a/src/FileSystem.h +++ b/src/FileSystem.h @@ -194,6 +194,58 @@ namespace FileSystem { virtual ~FileDataMalloc() { std::free(m_data); } }; + + class FileEnumerator { + public: + enum Flags { + IncludeDirs = 1, + IncludeSpecials = 2, + ExcludeFiles = 4, + Recurse = 8 + }; + + // Iterator interface for use with C++11 range-for only + struct iter { + FileEnumerator &m_enum; + bool m_atend = false; + + using iterator_category = std::input_iterator_tag; + using reference = const FileInfo &; + using value_type = const FileInfo; + + reference operator*() const { return m_enum.Current(); } + bool operator!=(const iter &rhs) { return m_enum.Finished() != rhs.m_atend; } + + iter &operator++() + { + m_enum.Next(); + return *this; + } + }; + + explicit FileEnumerator(FileSource &fs, int flags = 0); + explicit FileEnumerator(FileSource &fs, const std::string &path, int flags = 0); + ~FileEnumerator(); + + void AddSearchRoot(const std::string &path); + + bool Finished() const { return m_queue.empty(); } + void Next(); + const FileInfo &Current() const { return m_queue.front(); } + + iter begin() { return iter{ *this, false }; } + iter end() { return iter{ *this, true }; } + + private: + void ExpandDirQueue(); + void QueueDirectoryContents(const FileInfo &info); + + FileSource *m_source; + std::deque m_queue; + std::deque m_dirQueue; + int m_flags; + }; + class FileSource { public: explicit FileSource(const std::string &root, bool trusted = false) : @@ -207,6 +259,16 @@ namespace FileSystem { virtual RefCountedPtr ReadFile(const std::string &path) = 0; virtual bool ReadDirectory(const std::string &path, std::vector &output) = 0; + virtual FileEnumerator Enumerate(int enumeratorFlags) + { + return FileEnumerator(*this, enumeratorFlags); + } + + virtual FileEnumerator Enumerate(const std::string &path, int enumeratorFlags) + { + return FileEnumerator(*this, path, enumeratorFlags); + } + bool IsTrusted() const { return m_trusted; } protected: @@ -260,57 +322,6 @@ namespace FileSystem { std::vector m_sources; }; - class FileEnumerator { - public: - enum Flags { - IncludeDirs = 1, - IncludeSpecials = 2, - ExcludeFiles = 4, - Recurse = 8 - }; - - // Iterator interface for use with C++11 range-for only - struct iter { - FileEnumerator &m_enum; - bool m_atend = false; - - using iterator_category = std::input_iterator_tag; - using reference = const FileInfo &; - using value_type = const FileInfo; - - reference operator*() const { return m_enum.Current(); } - bool operator!=(const iter &rhs) { return m_enum.Finished() != rhs.m_atend; } - - iter &operator++() - { - m_enum.Next(); - return *this; - } - }; - - explicit FileEnumerator(FileSource &fs, int flags = 0); - explicit FileEnumerator(FileSource &fs, const std::string &path, int flags = 0); - ~FileEnumerator(); - - void AddSearchRoot(const std::string &path); - - bool Finished() const { return m_queue.empty(); } - void Next(); - const FileInfo &Current() const { return m_queue.front(); } - - iter begin() { return iter{ *this, false }; } - iter end() { return iter{ *this, true }; } - - private: - void ExpandDirQueue(); - void QueueDirectoryContents(const FileInfo &info); - - FileSource *m_source; - std::deque m_queue; - std::deque m_dirQueue; - int m_flags; - }; - } // namespace FileSystem inline std::string FileSystem::FileInfo::GetAbsoluteDir() const diff --git a/src/ModManager.cpp b/src/ModManager.cpp index b3f92d00834..2ef7863cba8 100644 --- a/src/ModManager.cpp +++ b/src/ModManager.cpp @@ -6,19 +6,67 @@ #include "FileSystem.h" #include "utils.h" +std::vector ModManager::m_loadedMods; + void ModManager::Init() { FileSystem::userFiles.MakeDirectory("mods"); +} + +void ModManager::Uninit() +{ + m_loadedMods.clear(); +} + +void ModManager::LoadMods(IniConfig *config) +{ + auto files = FileSystem::userFiles.Enumerate("mods", FileSystem::FileEnumerator::IncludeDirs); + for (const FileSystem::FileInfo &fileInfo : files) { + ModInfo newMod = {}; + newMod.name = fileInfo.GetName(); + newMod.path = FileSystem::JoinPath(FileSystem::userFiles.GetRoot(), fileInfo.GetPath()); + + if (ends_with_ci(fileInfo.GetPath(), ".zip")) { + newMod.name = newMod.name.substr(0, newMod.name.size() - 4); + newMod.fs = std::make_unique(FileSystem::userFiles, fileInfo.GetPath()); + } else if (fileInfo.IsDir()) { + newMod.fs = std::make_unique(newMod.path); + } + + // Not a valid mod if we don't have a modfs for it + if (!newMod.fs) + continue; - for (FileSystem::FileEnumerator files(FileSystem::userFiles, "mods", FileSystem::FileEnumerator::IncludeDirs); !files.Finished(); files.Next()) { - const FileSystem::FileInfo &info = files.Current(); - const std::string &zipPath = info.GetPath(); - if (ends_with_ci(zipPath, ".zip")) { - Output("adding mod: %s\n", zipPath.c_str()); - FileSystem::gameDataFiles.PrependSource(new FileSystem::FileSourceZip(FileSystem::userFiles, zipPath)); - } else if (info.IsDir()) { - Output("adding mod: %s\n", zipPath.c_str()); - FileSystem::gameDataFiles.PrependSource(new FileSystem::FileSourceFS(FileSystem::JoinPath(FileSystem::userFiles.GetRoot(), zipPath))); + newMod.enabled = !compare_ci(config->String("ModLoader", newMod.name, ""), "disabled"); + newMod.loadOrder = config->Int("ModLoader", newMod.name, m_loadedMods.size()); + + Log::Info("Found mod {}, enabled: {}", newMod.name, newMod.enabled); + Log::Info("\tsource: {}", newMod.path); + + // Don't load the mod if it's been disabled + m_loadedMods.emplace_back(std::move(newMod)); + } + + ReorderMods(config); + + Log::Info("Loaded mods:"); + for (const auto &modInfo : m_loadedMods) { + if (modInfo.enabled) { + Log::Info("\t{}", modInfo.name); + FileSystem::gameDataFiles.PrependSource(modInfo.fs.get()); } } } + +void ModManager::ReorderMods(IniConfig *config) +{ + std::sort(m_loadedMods.begin(), m_loadedMods.end(), [](const auto &a, const auto &b){ return a.loadOrder < b.loadOrder; }); + + for (const auto &modInfo : m_loadedMods) { + // write back the mod load order to the config file + if (modInfo.enabled) + config->SetInt("ModLoader", modInfo.name, modInfo.loadOrder); + else + config->SetString("ModLoader", modInfo.name, "disabled"); + } +} diff --git a/src/ModManager.h b/src/ModManager.h index 9bdfcf8a9a5..24ef8583430 100644 --- a/src/ModManager.h +++ b/src/ModManager.h @@ -4,12 +4,37 @@ #ifndef _MODMANAGER_H #define _MODMANAGER_H +#include "FileSystem.h" +#include "core/IniConfig.h" + +#include +#include + // right now this is little more than a stub class to hook up zipfiles to the // virtual filesystem class ModManager { public: + struct ModInfo { + std::string name; + std::string path; + std::unique_ptr fs; + mutable int loadOrder; + bool enabled; + }; + static void Init(); + static void Uninit(); + + static void LoadMods(IniConfig *config); + static void ReorderMods(IniConfig *config); + + static const std::vector &EnumerateMods() { return m_loadedMods; } + +private: + + static std::vector m_loadedMods; + }; #endif diff --git a/src/Pi.cpp b/src/Pi.cpp index aca70e2a62e..942cc793a02 100644 --- a/src/Pi.cpp +++ b/src/Pi.cpp @@ -336,6 +336,7 @@ void Pi::App::OnStartup() Output("%s\n", OS::GetOSInfoString().c_str()); ModManager::Init(); + ModManager::LoadMods(config); Lang::Resource &res(Lang::GetResource("core", config->String("Lang"))); Lang::MakeCore(res); @@ -447,6 +448,8 @@ void Pi::App::OnShutdown() BodyComponentDB::Uninit(); + ModManager::Uninit(); + ShutdownRenderer(); Pi::renderer = nullptr; diff --git a/src/editor/EditorApp.cpp b/src/editor/EditorApp.cpp index e9ffcdb9719..ce557116631 100644 --- a/src/editor/EditorApp.cpp +++ b/src/editor/EditorApp.cpp @@ -81,6 +81,8 @@ void EditorApp::OnStartup() ModManager::Init(); + ModManager::LoadMods(&cfg); + // get threads up Uint32 numThreads = cfg.Int("WorkerThreads"); numThreads = numThreads ? numThreads : std::max(OS::GetNumCores() - 1, 1U); @@ -106,6 +108,8 @@ void EditorApp::OnShutdown() Lua::Uninit(); Graphics::Uninit(); + ModManager::Uninit(); + ShutdownPiGui(); ShutdownRenderer(); ShutdownInput(); From c6724dd38bd81ca6fa472f7cb213c5c960308dd8 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 30 Aug 2023 03:45:08 -0400 Subject: [PATCH 3/7] SystemView: separate viewed / selected API - Optionally display and allow centering on object gravpoints. - Decouple the selected object from the viewed object and allow setting both independently. SystemView: handle drawing without system ptr --- src/SystemView.cpp | 36 ++++++++++++++++++++---------------- src/SystemView.h | 6 +++++- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/SystemView.cpp b/src/SystemView.cpp index 54873874661..e438b37986e 100644 --- a/src/SystemView.cpp +++ b/src/SystemView.cpp @@ -285,6 +285,7 @@ SystemMapViewport::SystemMapViewport(GuiApplication *app) : m_app(app), m_renderer(app->GetRenderer()), m_displayMode(SystemView::Mode::Orrery), + m_showGravpoints(false), m_showL4L5(LAG_OFF), m_shipDrawing(OFF), m_gridDrawing(GridDrawing::OFF), @@ -377,10 +378,12 @@ void SystemMapViewport::SetCurrentSystem(RefCountedPtr system) m_system = system; - SystemBody *body = m_system->GetRootBody().Get(); - m_atlasLayout = {}; - m_atlasLayout.isVertical = body->GetType() == SystemBody::TYPE_GRAVPOINT; - LayoutSystemBody(body, m_atlasLayout); + if (m_system) { + SystemBody *body = m_system->GetRootBody().Get(); + m_atlasLayout = {}; + m_atlasLayout.isVertical = body->GetType() == SystemBody::TYPE_GRAVPOINT; + LayoutSystemBody(body, m_atlasLayout); + } ResetViewpoint(); } @@ -476,9 +479,8 @@ void SystemMapViewport::AddBodyTrack(const SystemBody *b, const vector3d &offset if (b->GetType() == SystemBody::TYPE_STARPORT_SURFACE) return; - if (b->GetType() != SystemBody::TYPE_GRAVPOINT) { + if (b->GetType() != SystemBody::TYPE_GRAVPOINT || m_showGravpoints) AddObjectTrack({ Projectable::OBJECT, Projectable::SYSTEMBODY, b, offset }); - } // perfect-knowledge abstraction: track all child bodies if (b->HasChildren()) { @@ -492,13 +494,12 @@ void SystemMapViewport::AddBodyTrack(const SystemBody *b, const vector3d &offset continue; } - if (is_zero_general(kid->GetOrbit().GetSemiMajorAxis())) - return; - - // Add the body's orbit - Projectable p(Projectable::ORBIT, Projectable::SYSTEMBODY, kid); - p.worldpos = offset; - AddOrbitTrack(p, &kid->GetOrbit(), svColor[SYSTEMBODY_ORBIT], 0.0); + if (!is_zero_general(kid->GetOrbit().GetSemiMajorAxis())) { + // Add the body's orbit + Projectable p(Projectable::ORBIT, Projectable::SYSTEMBODY, kid); + p.worldpos = offset; + AddOrbitTrack(p, &kid->GetOrbit(), svColor[SYSTEMBODY_ORBIT], 0.0); + } // not using current time yet AddBodyTrack(kid, offset + kid->GetOrbit().OrbitalPosAtTime(m_time)); @@ -572,6 +573,9 @@ void SystemMapViewport::DrawOrreryView() m_background->SetIntensity(0.6); m_background->Draw(trans2bg); + if (!m_system) + return; + m_renderer->ClearDepthBuffer(); // We need to adjust the "far" cutoff plane, so that at high magnifications you can see @@ -915,7 +919,7 @@ void SystemMapViewport::Update(float ft) } if (m_displayMode == SystemView::Mode::Orrery) { - if (!m_system->GetUnexplored() && m_system->GetRootBody()) { + if (m_system && !m_system->GetUnexplored() && m_system->GetRootBody()) { // all systembodies draws here AddBodyTrack(m_system->GetRootBody().Get(), vector3d(0, 0, 0)); } @@ -1120,12 +1124,12 @@ void SystemMapViewport::ClearSelectedObject() m_selectedObject.type = Projectable::NONE; } -void SystemMapViewport::ViewSelectedObject() +void SystemMapViewport::SetViewedObject(Projectable p) { // we will immediately determine the coordinates of the viewed body so that // there is a correct starting point of the transition animation, otherwise // there may be an unwanted shift in the next frame - m_viewedObject = m_selectedObject; + m_viewedObject = p; m_animateTransition = MAX_TRANSITION_FRAMES; } diff --git a/src/SystemView.h b/src/SystemView.h index f9b079e9abd..bab2c27147a 100644 --- a/src/SystemView.h +++ b/src/SystemView.h @@ -240,7 +240,8 @@ class SystemMapViewport { void SetSelectedObject(Projectable p) { m_selectedObject = p; } void ClearSelectedObject(); - void ViewSelectedObject(); + void ViewSelectedObject() { SetViewedObject(m_selectedObject); } + void SetViewedObject(Projectable p); void ResetViewpoint(); Projectable *GetViewedObject() { return &m_viewedObject; } @@ -276,6 +277,8 @@ class SystemMapViewport { void SetRotateMode(bool enable); void SetDisplayMode(SystemView::Mode displayMode) { m_displayMode = displayMode; } void SetBackground(Background::Container *bg) { m_background = bg; } + void SetShowGravpoints(bool enabled) { m_showGravpoints = enabled; } + double ProjectedSize(double size, vector3d pos); float AtlasViewPlanetGap(float planetRadius) { return std::max(planetRadius * 0.6, 1.33); } float AtlasViewPixelPerUnit(); @@ -348,6 +351,7 @@ class SystemMapViewport { vector2f m_atlasPos, m_atlasPosTo, m_atlasPosDefault; float m_atlasViewW, m_atlasViewH; + bool m_showGravpoints; ShowLagrange m_showL4L5; ShipDrawing m_shipDrawing; GridDrawing m_gridDrawing; From 9f08c0592608c7b06f56589e7953c124c047c1ec Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 30 Aug 2023 05:04:34 -0400 Subject: [PATCH 4/7] EditorApp: make sure to initialize Lang --- src/editor/EditorApp.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/editor/EditorApp.cpp b/src/editor/EditorApp.cpp index ce557116631..de704e200f5 100644 --- a/src/editor/EditorApp.cpp +++ b/src/editor/EditorApp.cpp @@ -6,6 +6,7 @@ #include "EditorDraw.h" #include "FileSystem.h" +#include "Lang.h" #include "ModManager.h" #include "ModelViewer.h" @@ -88,6 +89,9 @@ void EditorApp::OnStartup() numThreads = numThreads ? numThreads : std::max(OS::GetNumCores() - 1, 1U); GetTaskGraph()->SetWorkerThreads(numThreads); + Lang::Resource &res(Lang::GetResource("core", cfg.String("Lang"))); + Lang::MakeCore(res); + Graphics::RendererOGL::RegisterRenderer(); m_renderer = StartupRenderer(&cfg); From 6dbdfd61ef1e3720bc244fd0ed824cf14e3e81e8 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 30 Aug 2023 05:10:07 -0400 Subject: [PATCH 5/7] Add Portable File Dialogs library Licensed under WTFPLv2, selected to implement editor file-picker interface. --- AUTHORS.txt | 4 + contrib/portable-file-dialogs/pfd.h | 1887 +++++++++++++++++++++++++++ 2 files changed, 1891 insertions(+) create mode 100644 contrib/portable-file-dialogs/pfd.h diff --git a/AUTHORS.txt b/AUTHORS.txt index b773495607a..71f451b1c50 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -203,6 +203,10 @@ Pioneer includes the following third-party software: Copyright (c) 2019 Stanislav Denisov Licensed under the MIT license + Portable File Dialogs + Copyright © 2018–2022 Sam Hocevar + Licensed under the WTFPLv2 license (http://www.wtfpl.net/) + High Performance C++ Profiler https://code.google.com/p/high-performance-cplusplus-profiler/ Licensed under the MIT license diff --git a/contrib/portable-file-dialogs/pfd.h b/contrib/portable-file-dialogs/pfd.h new file mode 100644 index 00000000000..1fc79a29103 --- /dev/null +++ b/contrib/portable-file-dialogs/pfd.h @@ -0,0 +1,1887 @@ +// +// Portable File Dialogs +// +// Copyright © 2018–2022 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async +#include // GetUserProfileDirectory() + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 2 // for popen() +#endif +#ifdef __APPLE__ +# ifndef _DARWIN_C_SOURCE +# define _DARWIN_C_SOURCE +# endif +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2(), getuid() +#include // ::kill, std::signal +#include // stat() +#include // waitpid() +#include // getpwnam() +#endif + +#include // std::string +#include // std::shared_ptr +#include // std::ostream +#include // std::map +#include // std::set +#include // std::regex +#include // std::mutex, std::this_thread +#include // std::chrono + +// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog +#ifndef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 1 +# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION +# if __GXX_ABI_VERSION <= 1013 +# undef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 0 +# endif +# endif +#endif + +namespace pfd +{ + +enum class button +{ + cancel = -1, + ok, + yes, + no, + abort, + retry, + ignore, +}; + +enum class choice +{ + ok = 0, + ok_cancel, + yes_no, + yes_no_cancel, + retry_cancel, + abort_retry_ignore, +}; + +enum class icon +{ + info = 0, + warning, + error, + question, +}; + +// Additional option flags for various dialog constructors +enum class opt : uint8_t +{ + none = 0, + // For file open, allow multiselect. + multiselect = 0x1, + // For file save, force overwrite and disable the confirmation dialog. + force_overwrite = 0x2, + // For folder select, force path to be the provided argument instead + // of the last opened directory, which is the Microsoft-recommended, + // user-friendly behaviour. + force_path = 0x4, +}; + +inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } +inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } + +// The settings class, only exposing to the user a way to set verbose mode +// and to force a rescan of installed desktop helpers (zenity, kdialog…). +class settings +{ +public: + static bool available(); + + static void verbose(bool value); + static void rescan(); + +protected: + explicit settings(bool resync = false); + + bool check_program(std::string const &program); + + inline bool is_osascript() const; + inline bool is_zenity() const; + inline bool is_kdialog() const; + + enum class flag + { + is_scanned = 0, + is_verbose, + + has_zenity, + has_matedialog, + has_qarma, + has_kdialog, + is_vista, + + max_flag, + }; + + // Static array of flags for internal state + bool const &flags(flag in_flag) const; + + // Non-const getter for the static array of flags + bool &flags(flag in_flag); +}; + +// Internal classes, not to be used by client applications +namespace internal +{ + +// Process wait timeout, in milliseconds +static int const default_wait_timeout = 20; + +class executor +{ + friend class dialog; + +public: + // High level function to get the result of a command + std::string result(int *exit_code = nullptr); + + // High level function to abort + bool kill(); + +#if _WIN32 + void start_func(std::function const &fun); + static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); +#elif __EMSCRIPTEN__ + void start(int exit_code); +#else + void start_process(std::vector const &command); +#endif + + ~executor(); + +protected: + bool ready(int timeout = default_wait_timeout); + void stop(); + +private: + bool m_running = false; + std::string m_stdout; + int m_exit_code = -1; +#if _WIN32 + std::future m_future; + std::set m_windows; + std::condition_variable m_cond; + std::mutex m_mutex; + DWORD m_tid; +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something +#else + pid_t m_pid = 0; + int m_fd = -1; +#endif +}; + +class platform +{ +protected: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll + { + public: + dll(std::string const &name); + ~dll(); + + template class proc + { + public: + proc(dll const &lib, std::string const &sym) + : m_proc(reinterpret_cast((void *)::GetProcAddress(lib.handle, sym.c_str()))) + {} + + operator bool() const { return m_proc != nullptr; } + operator T *() const { return m_proc; } + + private: + T *m_proc; + }; + + private: + HMODULE handle; + }; + + // Helper class around CoInitialize() and CoUnInitialize() + class ole32_dll : public dll + { + public: + ole32_dll(); + ~ole32_dll(); + bool is_initialized(); + + private: + HRESULT m_state; + }; + + // Helper class around CreateActCtx() and ActivateActCtx() + class new_style_context + { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +class dialog : protected settings, protected platform +{ +public: + bool ready(int timeout = default_wait_timeout) const; + bool kill() const; + +protected: + explicit dialog(); + + std::vector desktop_helper() const; + static std::string buttons_to_name(choice _choice); + static std::string get_icon_name(icon _icon); + + std::string powershell_quote(std::string const &str) const; + std::string osascript_quote(std::string const &str) const; + std::string shell_quote(std::string const &str) const; + + // Keep handle to executing command + std::shared_ptr m_async; +}; + +class file_dialog : public dialog +{ +protected: + enum type + { + open, + save, + folder, + }; + + file_dialog(type in_type, + std::string const &title, + std::string const &default_path = "", + std::vector const &filters = {}, + opt options = opt::none); + +protected: + std::string string_result(); + std::vector vector_result(); + +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); +#if PFD_HAS_IFILEDIALOG + std::string select_folder_vista(IFileDialog *ifd, bool force_path); +#endif + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +} // namespace internal + +// +// The path class provides some platform-specific path constants +// + +class path : protected internal::platform +{ +public: + static std::string home(); + static std::string separator(); +}; + +// +// The notify widget +// + +class notify : public internal::dialog +{ +public: + notify(std::string const &title, + std::string const &message, + icon _icon = icon::info); +}; + +// +// The message widget +// + +class message : public internal::dialog +{ +public: + message(std::string const &title, + std::string const &text, + choice _choice = choice::ok_cancel, + icon _icon = icon::info); + + button result(); + +private: + // Some extra logic to map the exit code to button number + std::map m_mappings; +}; + +// +// The open_file, save_file, and open_folder widgets +// + +class open_file : public internal::file_dialog +{ +public: + open_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] +#endif +#endif + open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect); + + std::vector result(); +}; + +class save_file : public internal::file_dialog +{ +public: + save_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] +#endif +#endif + save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite); + + std::string result(); +}; + +class select_folder : public internal::file_dialog +{ +public: + select_folder(std::string const &title, + std::string const &default_path = "", + opt options = opt::none); + + std::string result(); +}; + +// +// Below this are all the method implementations. You may choose to define the +// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except +// in one place. This may reduce compilation times. +// + +#if !defined PFD_SKIP_IMPLEMENTATION + +// internal free functions implementations + +namespace internal +{ + +#if _WIN32 +static inline std::wstring str2wstr(std::string const &str) +{ + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring ret(len, '\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); + return ret; +} + +static inline std::string wstr2str(std::wstring const &str) +{ + int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); + std::string ret(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); + return ret; +} + +static inline bool is_vista() +{ + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + DWORDLONG const mask = VerSetConditionMask( + VerSetConditionMask( + VerSetConditionMask( + 0, VER_MAJORVERSION, VER_GREATER_EQUAL), + VER_MINORVERSION, VER_GREATER_EQUAL), + VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); + osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); + osvi.wServicePackMajor = 0; + + return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; +} +#endif + +// This is necessary until C++20 which will have std::string::ends_with() etc. + +static inline bool ends_with(std::string const &str, std::string const &suffix) +{ + return suffix.size() <= str.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static inline bool starts_with(std::string const &str, std::string const &prefix) +{ + return prefix.size() <= str.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +// This is necessary until C++17 which will have std::filesystem::is_directory + +static inline bool is_directory(std::string const &path) +{ +#if _WIN32 + auto attr = GetFileAttributesA(path.c_str()); + return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY); +#elif __EMSCRIPTEN__ + // TODO + return false; +#else + struct stat s; + return stat(path.c_str(), &s) == 0 && S_ISDIR(s.st_mode); +#endif +} + +// This is necessary because getenv is not thread-safe + +static inline std::string getenv(std::string const &str) +{ +#if _MSC_VER + char *buf = nullptr; + size_t size = 0; + if (_dupenv_s(&buf, &size, str.c_str()) == 0 && buf) + { + std::string ret(buf); + free(buf); + return ret; + } + return ""; +#else + auto buf = std::getenv(str.c_str()); + return buf ? buf : ""; +#endif +} + +} // namespace internal + +// settings implementation + +inline settings::settings(bool resync) +{ + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + + auto pfd_verbose = internal::getenv("PFD_VERBOSE"); + auto match_no = std::regex("(|0|no|false)", std::regex_constants::icase); + if (!std::regex_match(pfd_verbose, match_no)) + flags(flag::is_verbose) = true; + +#if _WIN32 + flags(flag::is_vista) = internal::is_vista(); +#elif !__APPLE__ + flags(flag::has_zenity) = check_program("zenity"); + flags(flag::has_matedialog) = check_program("matedialog"); + flags(flag::has_qarma) = check_program("qarma"); + flags(flag::has_kdialog) = check_program("kdialog"); + + // If multiple helpers are available, try to default to the best one + if (flags(flag::has_zenity) && flags(flag::has_kdialog)) + { + auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +inline bool settings::available() +{ +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#elif __EMSCRIPTEN__ + // FIXME: Return true after implementation is complete. + return false; +#else + settings tmp; + return tmp.flags(flag::has_zenity) || + tmp.flags(flag::has_matedialog) || + tmp.flags(flag::has_qarma) || + tmp.flags(flag::has_kdialog); +#endif +} + +inline void settings::verbose(bool value) +{ + settings().flags(flag::is_verbose) = value; +} + +inline void settings::rescan() +{ + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +inline bool settings::check_program(std::string const &program) +{ +#if _WIN32 + (void)program; + return false; +#elif __EMSCRIPTEN__ + (void)program; + return false; +#else + int exit_code = -1; + internal::executor async; + async.start_process({"/bin/sh", "-c", "which " + program}); + async.result(&exit_code); + return exit_code == 0; +#endif +} + +inline bool settings::is_osascript() const +{ +#if __APPLE__ + return true; +#else + return false; +#endif +} + +inline bool settings::is_zenity() const +{ + return flags(flag::has_zenity) || + flags(flag::has_matedialog) || + flags(flag::has_qarma); +} + +inline bool settings::is_kdialog() const +{ + return flags(flag::has_kdialog); +} + +inline bool const &settings::flags(flag in_flag) const +{ + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +inline bool &settings::flags(flag in_flag) +{ + return const_cast(static_cast(this)->flags(in_flag)); +} + +// path implementation +inline std::string path::home() +{ +#if _WIN32 + // First try the USERPROFILE environment variable + auto user_profile = internal::getenv("USERPROFILE"); + if (user_profile.size() > 0) + return user_profile; + // Otherwise, try GetUserProfileDirectory() + HANDLE token = nullptr; + DWORD len = MAX_PATH; + char buf[MAX_PATH] = { '\0' }; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + { + dll userenv("userenv.dll"); + dll::proc get_user_profile_directory(userenv, "GetUserProfileDirectoryA"); + get_user_profile_directory(token, buf, &len); + CloseHandle(token); + if (*buf) + return buf; + } +#elif __EMSCRIPTEN__ + return "/"; +#else + // First try the HOME environment variable + auto home = internal::getenv("HOME"); + if (home.size() > 0) + return home; + // Otherwise, try getpwuid_r() + size_t len = 4096; +#if defined(_SC_GETPW_R_SIZE_MAX) + auto size_max = sysconf(_SC_GETPW_R_SIZE_MAX); + if (size_max != -1) + len = size_t(size_max); +#endif + std::vector buf(len); + struct passwd pwd, *result; + if (getpwuid_r(getuid(), &pwd, buf.data(), buf.size(), &result) == 0) + return result->pw_dir; +#endif + return "/"; +} + +inline std::string path::separator() +{ +#if _WIN32 + return "\\"; +#else + return "/"; +#endif +} + +// executor implementation + +inline std::string internal::executor::result(int *exit_code /* = nullptr */) +{ + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +inline bool internal::executor::kill() +{ +#if _WIN32 + if (m_future.valid()) + { + // Close all windows that weren’t open when we started the future + auto previous_windows = m_windows; + EnumWindows(&enum_windows_callback, (LPARAM)this); + for (auto hwnd : m_windows) + if (previous_windows.find(hwnd) == previous_windows.end()) + { + SendMessage(hwnd, WM_CLOSE, 0, 0); + // Also send IDNO in case of a Yes/No or Abort/Retry/Ignore messagebox + SendMessage(hwnd, WM_COMMAND, IDNO, 0); + } + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) +{ + auto that = (executor *)lParam; + + DWORD pid; + auto tid = GetWindowThreadProcessId(hwnd, &pid); + if (tid == that->m_tid) + that->m_windows.insert(hwnd); + return TRUE; +} +#endif + +#if _WIN32 +inline void internal::executor::start_func(std::function const &fun) +{ + stop(); + + auto trampoline = [fun, this]() + { + // Save our thread id so that the caller can cancel us + m_tid = GetCurrentThreadId(); + EnumWindows(&enum_windows_callback, (LPARAM)this); + m_cond.notify_all(); + return fun(&m_exit_code); + }; + + std::unique_lock lock(m_mutex); + m_future = std::async(std::launch::async, trampoline); + m_cond.wait(lock); + m_running = true; +} + +#elif __EMSCRIPTEN__ +inline void internal::executor::start(int exit_code) +{ + m_exit_code = exit_code; +} + +#else +inline void internal::executor::start_process(std::vector const &command) +{ + stop(); + m_stdout.clear(); + m_exit_code = -1; + + int in[2], out[2]; + if (pipe(in) != 0 || pipe(out) != 0) + return; + + m_pid = fork(); + if (m_pid < 0) + return; + + close(in[m_pid ? 0 : 1]); + close(out[m_pid ? 1 : 0]); + + if (m_pid == 0) + { + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + + // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) + int fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector args; + std::transform(command.cbegin(), command.cend(), std::back_inserter(args), + [](std::string const &s) { return const_cast(s.c_str()); }); + args.push_back(nullptr); // null-terminate argv[] + + execvp(args[0], args.data()); + exit(1); + } + + close(in[1]); + m_fd = out[0]; + auto flags = fcntl(m_fd, F_GETFL); + fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); + + m_running = true; +} +#endif + +inline internal::executor::~executor() +{ + stop(); +} + +inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) +{ + if (!m_running) + return true; + +#if _WIN32 + if (m_future.valid()) + { + auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) + { + // On Windows, we need to run the message pump. If the async + // thread uses a Windows API dialog, it may be attached to the + // main thread and waiting for messages that only we can dispatch. + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return false; + } + + m_stdout = m_future.get(); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; +#else + char buf[BUFSIZ]; + ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore + if (received > 0) + { + m_stdout += std::string(buf, received); + return false; + } + + // Reap child process if it is dead. It is possible that the system has already reaped it + // (this happens when the calling application handles or ignores SIG_CHLD) and results in + // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for + // a little while. + int status; + pid_t child = waitpid(m_pid, &status, WNOHANG); + if (child != m_pid && (child >= 0 || errno != ECHILD)) + { + // FIXME: this happens almost always at first iteration + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return false; + } + + close(m_fd); + m_exit_code = WEXITSTATUS(status); +#endif + + m_running = false; + return true; +} + +inline void internal::executor::stop() +{ + // Loop until the user closes the dialog + while (!ready()) + ; +} + +// dll implementation + +#if _WIN32 +inline internal::platform::dll::dll(std::string const &name) + : handle(::LoadLibraryA(name.c_str())) +{} + +inline internal::platform::dll::~dll() +{ + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// ole32_dll implementation + +#if _WIN32 +inline internal::platform::ole32_dll::ole32_dll() + : dll("ole32.dll") +{ + // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. + // See https://github.com/samhocevar/portable-file-dialogs/issues/51 + auto coinit = proc(*this, "CoInitializeEx"); + m_state = coinit(nullptr, COINIT_MULTITHREADED); +} + +inline internal::platform::ole32_dll::~ole32_dll() +{ + if (is_initialized()) + proc(*this, "CoUninitialize")(); +} + +inline bool internal::platform::ole32_dll::is_initialized() +{ + return m_state == S_OK || m_state == S_FALSE; +} +#endif + +// new_style_context implementation + +#if _WIN32 +inline internal::platform::new_style_context::new_style_context() +{ + // Only create one activation context for the whole app lifetime. + static HANDLE hctx = create(); + + if (hctx != INVALID_HANDLE_VALUE) + ActivateActCtx(hctx, &m_cookie); +} + +inline internal::platform::new_style_context::~new_style_context() +{ + DeactivateActCtx(0, m_cookie); +} + +inline HANDLE internal::platform::new_style_context::create() +{ + // This “hack” seems to be necessary for this code to work on windows XP. + // Without it, dialogs do not show and close immediately. GetError() + // returns 0 so I don’t know what causes this. I was not able to reproduce + // this behavior on Windows 7 and 10 but just in case, let it be here for + // those versions too. + // This hack is not required if other dialogs are used (they load comdlg32 + // automatically), only if message boxes are used. + dll comdlg32("comdlg32.dll"); + + // Using approach as shown here: https://stackoverflow.com/a/10444161 + UINT len = ::GetSystemDirectoryA(nullptr, 0); + std::string sys_dir(len, '\0'); + ::GetSystemDirectoryA(&sys_dir[0], len); + + ACTCTXA act_ctx = + { + // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a + // crash with error “default context is already set”. + sizeof(act_ctx), + ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, + "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, nullptr, 0, + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const +{ + return m_async->ready(timeout); +} + +inline bool internal::dialog::kill() const +{ + return m_async->kill(); +} + +inline internal::dialog::dialog() + : m_async(std::make_shared()) +{ +} + +inline std::vector internal::dialog::desktop_helper() const +{ +#if __APPLE__ + return { "osascript" }; +#else + return { flags(flag::has_zenity) ? "zenity" + : flags(flag::has_matedialog) ? "matedialog" + : flags(flag::has_qarma) ? "qarma" + : flags(flag::has_kdialog) ? "kdialog" + : "echo" }; +#endif +} + +inline std::string internal::dialog::buttons_to_name(choice _choice) +{ + switch (_choice) + { + case choice::ok_cancel: return "okcancel"; + case choice::yes_no: return "yesno"; + case choice::yes_no_cancel: return "yesnocancel"; + case choice::retry_cancel: return "retrycancel"; + case choice::abort_retry_ignore: return "abortretryignore"; + /* case choice::ok: */ default: return "ok"; + } +} + +inline std::string internal::dialog::get_icon_name(icon _icon) +{ + switch (_icon) + { + case icon::warning: return "warning"; + case icon::error: return "error"; + case icon::question: return "question"; + // Zenity wants "information" but WinForms wants "info" + /* case icon::info: */ default: +#if _WIN32 + return "info"; +#else + return "information"; +#endif + } +} + +// This is only used for debugging purposes +inline std::ostream& operator <<(std::ostream &s, std::vector const &v) +{ + int not_first = 0; + for (auto &e : v) + s << (not_first++ ? " " : "") << e; + return s; +} + +// Properly quote a string for Powershell: replace ' or " with '' or "" +// FIXME: we should probably get rid of newlines! +// FIXME: the \" sequence seems unsafe, too! +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::powershell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; +} + +// Properly quote a string for osascript: replace \ or " with \\ or \" +// XXX: this also used to replace ' with \' when popen was used, but it would be +// smarter to do shell_quote(osascript_quote(...)) if this is needed again. +inline std::string internal::dialog::osascript_quote(std::string const &str) const +{ + return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; +} + +// Properly quote a string for the shell: just replace ' with '\'' +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::shell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +inline internal::file_dialog::file_dialog(type in_type, + std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = {} */, + opt options /* = opt::none */) +{ +#if _WIN32 + std::string filter_list; + std::regex whitespace(" *"); + for (size_t i = 0; i + 1 < filters.size(); i += 2) + { + filter_list += filters[i] + '\0'; + filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; + } + filter_list += '\0'; + + m_async->start_func([this, in_type, title, default_path, filter_list, + options](int *exit_code) -> std::string + { + (void)exit_code; + m_wtitle = internal::str2wstr(title); + m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Initialise COM. This is required for the new folder selection window, + // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) + // and to avoid random crashes with GetOpenFileNameW() (see + // https://github.com/samhocevar/portable-file-dialogs/issues/51) + ole32_dll ole32; + + // Folder selection uses a different method + if (in_type == type::folder) + { +#if PFD_HAS_IFILEDIALOG + if (flags(flag::is_vista)) + { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog *ifd; + HRESULT hr = dll::proc(ole32, "CoCreateInstance") + (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); + + // In case CoCreateInstance fails (which it should not), try legacy approach + if (SUCCEEDED(hr)) + return select_folder_vista(ifd, options & opt::force_path); + } +#endif + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &bffcallback; + bi.lParam = (LPARAM)this; + + if (flags(flag::is_vista)) + { + if (ole32.is_initialized()) + bi.ulFlags |= BIF_NEWDIALOGSTYLE; + bi.ulFlags |= BIF_EDITBOX; + bi.ulFlags |= BIF_STATUSTEXT; + } + + auto *list = SHBrowseForFolderW(&bi); + std::string ret; + if (list) + { + auto buffer = new wchar_t[MAX_PATH]; + SHGetPathFromIDListW(list, buffer); + dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + return ret; + } + + OPENFILENAMEW ofn; + memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = GetActiveWindow(); + + ofn.lpstrFilter = wfilter_list.c_str(); + + auto woutput = std::wstring(MAX_PATH * 256, L'\0'); + ofn.lpstrFile = (LPWSTR)woutput.data(); + ofn.nMaxFile = (DWORD)woutput.size(); + if (!m_wdefault_path.empty()) + { + // If a directory was provided, use it as the initial directory. If + // a valid path was provided, use it as the initial file. Otherwise, + // let the Windows API decide. + auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_wdefault_path.c_str(); + else if (m_wdefault_path.size() <= woutput.size()) + //second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); + else + { + ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + dll comdlg32("comdlg32.dll"); + + // Apply new visual style (required for windows XP) + new_style_context ctx; + + if (in_type == type::save) + { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } + else + { + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); + if (get_open_file_name(&ofn) == 0) + return ""; + } + + std::string prefix; + for (wchar_t const *p = woutput.c_str(); *p; ) + { + auto filename = internal::wstr2str(p); + p += wcslen(p); + // In multiselect mode, we advance p one wchar further and + // check for another filename. If there is one and the + // prefix is empty, it means we just read the prefix. + if ((options & opt::multiselect) && *++p && prefix.empty()) + { + prefix = filename + "/"; + continue; + } + + m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#elif __EMSCRIPTEN__ + // FIXME: do something + (void)in_type; + (void)title; + (void)default_path; + (void)filters; + (void)options; +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "set ret to choose"; + switch (in_type) + { + case type::save: + script += " file name"; + break; + case type::open: default: + script += " file"; + if (options & opt::multiselect) + script += " with multiple selections allowed"; + break; + case type::folder: + script += " folder"; + break; + } + + if (default_path.size()) + { + if (in_type == type::folder || is_directory(default_path)) + script += " default location "; + else + script += " default name "; + script += osascript_quote(default_path); + } + + script += " with prompt " + osascript_quote(title); + + if (in_type == type::open) + { + // Concatenate all user-provided filter patterns + std::string patterns; + for (size_t i = 0; i < filters.size() / 2; ++i) + patterns += " " + filters[2 * i + 1]; + + // Split the pattern list to check whether "*" is in there; if it + // is, we have to disable filters because there is no mechanism in + // OS X for the user to override the filter. + std::regex sep("\\s+"); + std::string filter_list; + bool has_filter = true; + std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); + std::sregex_token_iterator end; + for ( ; iter != end; ++iter) + { + auto pat = iter->str(); + if (pat == "*" || pat == "*.*") + has_filter = false; + else if (internal::starts_with(pat, "*.")) + filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2)); + } + + if (has_filter && filter_list.size() > 0) + { + // There is a weird AppleScript bug where file extensions of length != 3 are + // ignored, e.g. type{"txt"} works, but type{"json"} does not. Fortunately if + // the whole list starts with a 3-character extension, everything works again. + // We use "///" for such an extension because we are sure it cannot appear in + // an actual filename. + script += " of type {\"///\"" + filter_list + "}"; + } + } + + if (in_type == type::open && (options & opt::multiselect)) + { + script += "\nset s to \"\""; + script += "\nrepeat with i in ret"; + script += "\n set s to s & (POSIX path of i) & \"\\n\""; + script += "\nend repeat"; + script += "\ncopy s to stdout"; + } + else + { + script += "\nPOSIX path of ret"; + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + command.push_back("--file-selection"); + + // If the default path is a directory, make sure it ends with "/" otherwise zenity will + // open the file dialog in the parent directory. + auto filename_arg = "--filename=" + default_path; + if (in_type != type::folder && !ends_with(default_path, "/") && internal::is_directory(default_path)) + filename_arg += "/"; + command.push_back(filename_arg); + + command.push_back("--title"); + command.push_back(title); + command.push_back("--separator=\n"); + + for (size_t i = 0; i < filters.size() / 2; ++i) + { + command.push_back("--file-filter"); + command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); + } + + if (in_type == type::save) + command.push_back("--save"); + if (in_type == type::folder) + command.push_back("--directory"); + if (!(options & opt::force_overwrite)) + command.push_back("--confirm-overwrite"); + if (options & opt::multiselect) + command.push_back("--multiple"); + } + else if (is_kdialog()) + { + switch (in_type) + { + case type::save: command.push_back("--getsavefilename"); break; + case type::open: command.push_back("--getopenfilename"); break; + case type::folder: command.push_back("--getexistingdirectory"); break; + } + if (options & opt::multiselect) + { + command.push_back("--multiple"); + command.push_back("--separate-output"); + } + + command.push_back(default_path); + + std::string filter; + for (size_t i = 0; i < filters.size() / 2; ++i) + filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; + command.push_back(filter); + + command.push_back("--title"); + command.push_back(title); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline std::string internal::file_dialog::string_result() +{ +#if _WIN32 + return m_async->result(); +#else + auto ret = m_async->result(); + // Strip potential trailing newline (zenity). Also strip trailing slash + // added by osascript for consistency with other backends. + while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) + ret.pop_back(); + return ret; +#endif +} + +inline std::vector internal::file_dialog::vector_result() +{ +#if _WIN32 + m_async->result(); + return m_vector_result; +#else + std::vector ret; + auto result = m_async->result(); + for (;;) + { + // Split result along newline characters + auto i = result.find('\n'); + if (i == 0 || i == std::string::npos) + break; + ret.push_back(result.substr(0, i)); + result = result.substr(i + 1, result.size()); + } + return ret; +#endif +} + +#if _WIN32 +// Use a static function to pass as BFFCALLBACK for legacy folder select +inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, + LPARAM, LPARAM pData) +{ + auto inst = (file_dialog *)pData; + switch (uMsg) + { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +#if PFD_HAS_IFILEDIALOG +inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) +{ + std::string result; + + IShellItem *folder; + + // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) + dll shell32("shell32.dll"); + dll::proc + create_item(shell32, "SHCreateItemFromParsingName"); + + if (!create_item) + return ""; + + auto hr = create_item(m_wdefault_path.c_str(), + nullptr, + IID_PPV_ARGS(&folder)); + + // Set default folder if found. This only sets the default folder. If + // Windows has any info about the most recently selected folder, it + // will display it instead. Generally, calling SetFolder() to set the + // current directory “is not a good or expected user experience and + // should therefore be avoided”: + // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder + if (SUCCEEDED(hr)) + { + if (force_path) + ifd->SetFolder(folder); + else + ifd->SetDefaultFolder(folder); + folder->Release(); + } + + // Set the dialog title and option to select folders + ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) + { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) + { + wchar_t* wname = nullptr; + // This is unlikely to fail because we use FOS_FORCEFILESYSTEM, but try + // to output a debug message just in case. + if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &wname))) + { + result = internal::wstr2str(std::wstring(wname)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wname); + } + else + { + if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname))) + { + auto name = internal::wstr2str(std::wstring(wname)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wname); + std::cerr << "pfd: failed to get path for " << name << std::endl; + } + else + std::cerr << "pfd: item of unknown type selected" << std::endl; + } + + item->Release(); + } + } + + ifd->Release(); + + return result; +} +#endif +#endif + +// notify implementation + +inline notify::notify(std::string const &title, + std::string const &message, + icon _icon /* = icon::info */) +{ + if (_icon == icon::question) // Not supported by notifications + _icon = icon::info; + +#if _WIN32 + // Use a static shared pointer for notify_icon so that we can delete + // it whenever we need to display a new one, and we can also wait + // until the program has finished running. + struct notify_icon_data : public NOTIFYICONDATAW + { + ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } + }; + + static std::shared_ptr nid; + + // Release the previous notification icon, if any, and allocate a new + // one. Note that std::make_shared() does value initialization, so there + // is no need to memset the structure. + nid = nullptr; + nid = std::make_shared(); + + // For XP support + nid->cbSize = NOTIFYICONDATAW_V2_SIZE; + nid->hWnd = nullptr; + nid->uID = 0; + + // Flag Description: + // - NIF_ICON The hIcon member is valid. + // - NIF_MESSAGE The uCallbackMessage member is valid. + // - NIF_TIP The szTip member is valid. + // - NIF_STATE The dwState and dwStateMask members are valid. + // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. + // - NIF_GUID Reserved. + nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; + + // Flag Description + // - NIIF_ERROR An error icon. + // - NIIF_INFO An information icon. + // - NIIF_NONE No icon. + // - NIIF_WARNING A warning icon. + // - NIIF_ICON_MASK Version 6.0. Reserved. + // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips + switch (_icon) + { + case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; + case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; + /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; + } + + ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL + { + ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); + return false; + }; + + nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); + + nid->uTimeout = 5000; + + StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); + StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); + + // Display the new icon + Shell_NotifyIconW(NIM_ADD, nid.get()); +#elif __EMSCRIPTEN__ + // FIXME: do something + (void)title; + (void)message; +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + command.push_back("-e"); + command.push_back("display notification " + osascript_quote(message) + + " with title " + osascript_quote(title)); + } + else if (is_zenity()) + { + command.push_back("--notification"); + command.push_back("--window-icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--text"); + command.push_back(title + "\n" + message); + } + else if (is_kdialog()) + { + command.push_back("--icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--title"); + command.push_back(title); + command.push_back("--passivepopup"); + command.push_back(message); + command.push_back("5"); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +// message implementation + +inline message::message(std::string const &title, + std::string const &text, + choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) +{ +#if _WIN32 + // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought + // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 + UINT style = MB_SYSTEMMODAL; + switch (_icon) + { + case icon::warning: style |= MB_ICONWARNING; break; + case icon::error: style |= MB_ICONERROR; break; + case icon::question: style |= MB_ICONQUESTION; break; + /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; + } + + switch (_choice) + { + case choice::ok_cancel: style |= MB_OKCANCEL; break; + case choice::yes_no: style |= MB_YESNO; break; + case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; + case choice::retry_cancel: style |= MB_RETRYCANCEL; break; + case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; + /* case choice::ok: */ default: style |= MB_OK; break; + } + + m_mappings[IDCANCEL] = button::cancel; + m_mappings[IDOK] = button::ok; + m_mappings[IDYES] = button::yes; + m_mappings[IDNO] = button::no; + m_mappings[IDABORT] = button::abort; + m_mappings[IDRETRY] = button::retry; + m_mappings[IDIGNORE] = button::ignore; + + m_async->start_func([text, title, style](int* exit_code) -> std::string + { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // Apply new visual style (required for all Windows versions) + new_style_context ctx; + *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); + return ""; + }); + +#elif __EMSCRIPTEN__ + std::string full_message; + switch (_icon) + { + case icon::warning: full_message = "⚠️"; break; + case icon::error: full_message = "⛔"; break; + case icon::question: full_message = "❓"; break; + /* case icon::info: */ default: full_message = "ℹ"; break; + } + + full_message += ' ' + title + "\n\n" + text; + + // This does not really start an async task; it just passes the + // EM_ASM_INT return value to a fake start() function. + m_async->start(EM_ASM_INT( + { + if ($1) + return window.confirm(UTF8ToString($0)) ? 0 : -1; + alert(UTF8ToString($0)); + return 0; + }, full_message.c_str(), _choice == choice::ok_cancel)); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "display dialog " + osascript_quote(text) + + " with title " + osascript_quote(title); + auto if_cancel = button::cancel; + switch (_choice) + { + case choice::ok_cancel: + script += "buttons {\"OK\", \"Cancel\"}" + " default button \"OK\"" + " cancel button \"Cancel\""; + break; + case choice::yes_no: + script += "buttons {\"Yes\", \"No\"}" + " default button \"Yes\"" + " cancel button \"No\""; + if_cancel = button::no; + break; + case choice::yes_no_cancel: + script += "buttons {\"Yes\", \"No\", \"Cancel\"}" + " default button \"Yes\"" + " cancel button \"Cancel\""; + break; + case choice::retry_cancel: + script += "buttons {\"Retry\", \"Cancel\"}" + " default button \"Retry\"" + " cancel button \"Cancel\""; + break; + case choice::abort_retry_ignore: + script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" + " default button \"Abort\"" + " cancel button \"Retry\""; + if_cancel = button::retry; + break; + case choice::ok: default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + if_cancel = button::ok; + break; + } + m_mappings[1] = if_cancel; + m_mappings[256] = if_cancel; // XXX: I think this was never correct + script += " with icon "; + switch (_icon) + { + #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ + "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" + case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; + case icon::warning: script += "caution"; break; + case icon::error: script += "stop"; break; + case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; + #undef PFD_OSX_ICON + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + switch (_choice) + { + case choice::ok_cancel: + command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; + case choice::yes_no: + // Do not use standard --question because it causes “No” to return -1, + // which is inconsistent with the “Yes/No/Cancel” mode below. + command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::yes_no_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::retry_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; + case choice::abort_retry_ignore: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; + case choice::ok: + default: + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--warning"); break; + default: command.push_back("--info"); break; + } + } + + command.insert(command.end(), { "--title", title, + "--width=300", "--height=0", // sensible defaults + "--no-markup", // do not interpret text as Pango markup + "--text", text, + "--icon-name=dialog-" + get_icon_name(_icon) }); + } + else if (is_kdialog()) + { + if (_choice == choice::ok) + { + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--sorry"); break; + default: command.push_back("--msgbox"); break; + } + } + else + { + std::string flag = "--"; + if (_icon == icon::warning || _icon == icon::error) + flag += "warning"; + flag += "yesno"; + if (_choice == choice::yes_no_cancel) + flag += "cancel"; + command.push_back(flag); + if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) + { + m_mappings[0] = button::yes; + m_mappings[256] = button::no; + } + } + + command.push_back(text); + command.push_back("--title"); + command.push_back(title); + + // Must be after the above part + if (_choice == choice::ok_cancel) + command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline button message::result() +{ + int exit_code; + auto ret = m_async->result(&exit_code); + // osascript will say "button returned:Cancel\n" + // and others will just say "Cancel\n" + if (internal::ends_with(ret, "Cancel\n")) + return button::cancel; + if (internal::ends_with(ret, "OK\n")) + return button::ok; + if (internal::ends_with(ret, "Yes\n")) + return button::yes; + if (internal::ends_with(ret, "No\n")) + return button::no; + if (internal::ends_with(ret, "Abort\n")) + return button::abort; + if (internal::ends_with(ret, "Retry\n")) + return button::retry; + if (internal::ends_with(ret, "Ignore\n")) + return button::ignore; + if (m_mappings.count(exit_code) != 0) + return m_mappings[exit_code]; + return exit_code == 0 ? button::ok : button::cancel; +} + +// open_file implementation + +inline open_file::open_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::open, title, default_path, filters, options) +{ +} + +inline open_file::open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect) + : open_file(title, default_path, filters, + (allow_multiselect ? opt::multiselect : opt::none)) +{ +} + +inline std::vector open_file::result() +{ + return vector_result(); +} + +// save_file implementation + +inline save_file::save_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::save, title, default_path, filters, options) +{ +} + +inline save_file::save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite) + : save_file(title, default_path, filters, + (confirm_overwrite ? opt::none : opt::force_overwrite)) +{ +} + +inline std::string save_file::result() +{ + return string_result(); +} + +// select_folder implementation + +inline select_folder::select_folder(std::string const &title, + std::string const &default_path /* = "" */, + opt options /* = opt::none */) + : file_dialog(type::folder, title, default_path, {}, options) +{ +} + +inline std::string select_folder::result() +{ + return string_result(); +} + +#endif // PFD_SKIP_IMPLEMENTATION + +} // namespace pfd From a746eb7d1cd42e2ed598fc25eb05d87763b49541 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Sep 2023 02:41:36 -0400 Subject: [PATCH 6/7] Load autoload.lua in "minimal" lua states --- data/libs/autoload.lua | 2 -- src/lua/Lua.cpp | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/libs/autoload.lua b/data/libs/autoload.lua index 15da553eb9e..19a5a53769b 100644 --- a/data/libs/autoload.lua +++ b/data/libs/autoload.lua @@ -4,8 +4,6 @@ -- this is the only library automatically loaded at startup -- its the right place to extend core Lua tables -require 'SpaceStation' - string.trim = function(s) return string.gsub(s or "", "^%s*(.-)%s*$", "%1") end diff --git a/src/lua/Lua.cpp b/src/lua/Lua.cpp index 31210eba9c6..23c23af4cee 100644 --- a/src/lua/Lua.cpp +++ b/src/lua/Lua.cpp @@ -66,6 +66,8 @@ namespace Lua { LuaColor::Register(manager->GetLuaState()); LuaEngine::Register(); + + pi_lua_dofile(manager->GetLuaState(), "libs/autoload.lua"); } void InitModules() @@ -122,7 +124,7 @@ namespace Lua { SceneGraph::Lua::Init(); // XXX load everything. for now, just modules - pi_lua_dofile(l, "libs/autoload.lua"); + // pi_lua_dofile(l, "libs/autoload.lua"); lua_pop(l, 1); pi_lua_import(l, "pigui", true); From 677545127759df8ae1b5896bca93a5be480afa62 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 7 Sep 2023 00:16:42 -0400 Subject: [PATCH 7/7] New for-loop based SplitString utility - Splits arbitrary input string_view based on delimiter characters - Replaces old SplitSpec/SplitString functionalities - General-purpose high-reliability string processing utility --- src/NavLights.cpp | 10 +++- src/core/StringUtils.cpp | 36 +++++------- src/core/StringUtils.h | 122 ++++++++++++++++++++++++++------------- src/main.cpp | 4 +- 4 files changed, 104 insertions(+), 68 deletions(-) diff --git a/src/NavLights.cpp b/src/NavLights.cpp index 1e93a02c0ac..b919d149588 100644 --- a/src/NavLights.cpp +++ b/src/NavLights.cpp @@ -33,8 +33,14 @@ static vector2f get_color(Uint8 c) static inline vector2f LoadLightColorUVoffset(const std::string &spec) { - std::vector v(2); - SplitSpec(spec, v); + std::vector v; + // parse float values in the spec + SplitString(spec, ",").to_vector(v, [](std::string_view str) { + return std::atof(std::string(str).c_str()); + }); + + // ensure the spec has enough values + v.resize(2); return vector2f(v[0], v[1]); } diff --git a/src/core/StringUtils.cpp b/src/core/StringUtils.cpp index 4c984397086..4044738d9f3 100644 --- a/src/core/StringUtils.cpp +++ b/src/core/StringUtils.cpp @@ -185,32 +185,22 @@ const char *pi_strcasestr(const char *haystack, const char *needle) } } -std::vector SplitString(const std::string &source, const std::string &delim) +size_t SplitString::step(std::string_view str) { - bool stringSplitted = false; - std::vector splitted; - - size_t startPos = 0; - do { - // try to find delim - size_t delimPos = source.find(delim, startPos); - - // if delim found - if (delimPos != std::string::npos) { - std::string element = source.substr(startPos, delimPos); - splitted.push_back(element); - - // prepare next loop - startPos = delimPos + delim.length(); - } else { - // push tail and exit - splitted.push_back(source.substr(startPos)); - stringSplitted = true; - } + return m_reverse ? str.find_last_of(m_delim) : str.find_first_of(m_delim); +} - } while (!stringSplitted); +void SplitString::trim(std::string_view &str, size_t next) +{ + if (next == std::string_view::npos) { + str = {}; + return; + } - return splitted; + if (m_reverse) + str.remove_suffix((str.size() + 1) - str.find_last_not_of(m_delim, next)); + else + str.remove_prefix(str.find_first_not_of(m_delim, next)); } std::string FloatToStr(float val) diff --git a/src/core/StringUtils.h b/src/core/StringUtils.h index a46033f057a..8fa9e53ac58 100644 --- a/src/core/StringUtils.h +++ b/src/core/StringUtils.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #ifdef _MSC_VER @@ -119,53 +120,92 @@ inline std::string_view strip_spaces(std::string_view &s) return s.substr(start, end); } -static inline size_t SplitSpec(const std::string &spec, std::vector &output) -{ - static const std::string delim(","); - - size_t i = 0, start = 0, end = 0; - while (end != std::string::npos) { - // get to the first non-delim char - start = spec.find_first_not_of(delim, end); - - // read the end, no more to do - if (start == std::string::npos) - break; - - // find the end - next delim or end of string - end = spec.find_first_of(delim, start); +// Utility class to split a string based on a provided set of delimiters +// Makes find_first_of / find_last_of more ergonomic to use +struct SplitString { + struct iter { + using value_type = std::string_view; + using reference = std::string_view; + + // "end" iterator + iter() : m_str(), m_parent(nullptr) {}; + // "live" iterator + iter(SplitString *parent) : + m_parent(parent), + m_str(parent->m_orig), + m_next(parent->step(m_str)) + { + } + + value_type operator*() + { + if (m_next != std::string_view::npos) + return m_parent->m_reverse ? m_str.substr(m_next + 1) : m_str.substr(0, m_next); + else + return m_str; + } + + iter &operator++() + { + m_parent->trim(m_str, m_next); + m_next = m_parent->step(m_str); + return *this; + } + + bool operator!=(const iter &rhs) { return !(*this == rhs); } + bool operator==(const iter &rhs) { + return (m_str.empty() && rhs.m_str.empty()) || + (m_parent == rhs.m_parent && m_str.size() == rhs.m_str.size()); + } + + private: + SplitString *m_parent; + std::string_view m_str; + size_t m_next = std::string_view::npos; + }; + + SplitString(std::string_view source, std::string_view delim) : + m_orig(source), m_delim(delim) + {} + + SplitString(std::string_view source, std::string_view delim, bool reverse) : + m_orig(source), m_delim(delim), m_reverse(reverse) + {} + + iter begin() { return iter(this); } + iter end() { return iter(); } + + // Split the input string to a vector of fragments using the specified type + template + std::vector to_vector() { + std::vector out; + for (auto str : *this) { + out.push_back(T(str)); + } - // extract the fragment and remember it - output[i++] = atoi(spec.substr(start, (end == std::string::npos) ? std::string::npos : end - start).c_str()); + return out; } - return i; -} - -static inline size_t SplitSpec(const std::string &spec, std::vector &output) -{ - static const std::string delim(","); - - size_t i = 0, start = 0, end = 0; - while (end != std::string::npos) { - // get to the first non-delim char - start = spec.find_first_not_of(delim, end); - - // read the end, no more to do - if (start == std::string::npos) - break; - - // find the end - next delim or end of string - end = spec.find_first_of(delim, start); - - // extract the fragment and remember it - output[i++] = atof(spec.substr(start, (end == std::string::npos) ? std::string::npos : end - start).c_str()); + // Apply the given predicate to each fragment of the string and push the result into the given container + template + void to_vector(Container &c, Predicate p) { + for (auto str : *this) { + c.push_back(p(str)); + } } - return i; -} +private: + friend struct iter; + + // find the boundary for the next token + size_t step(std::string_view str); + // remove previous substring if present + void trim(std::string_view &str, size_t next); -std::vector SplitString(const std::string &source, const std::string &delim); + std::string_view m_orig; + std::string_view m_delim; + bool m_reverse = false; +}; // 'Numeric type' to string conversions. std::string FloatToStr(float val); diff --git a/src/main.cpp b/src/main.cpp index 5cae87bd42b..2359184c003 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -122,7 +122,7 @@ extern "C" int main(int argc, char **argv) // fallthrough protect if (mode == MODE_START_AT) { // try to get start planet number - std::vector keyValue = SplitString(modeopt, "="); + std::vector keyValue = SplitString(modeopt, "=").to_vector(); // if found value if (keyValue.size() == 2) { @@ -158,7 +158,7 @@ extern "C" int main(int argc, char **argv) // for each argument for (; pos < argc; pos++) { const std::string arg(argv[pos]); - std::vector keyValue = SplitString(arg, "="); + std::vector keyValue = SplitString(arg, "=").to_vector(); // if there no key and value || key is empty || value is empty if (keyValue.size() != 2 || keyValue[0].empty() || keyValue[1].empty()) {