diff --git a/3rdparty/xdgpp/.builds/archlinux.yml b/3rdparty/xdgpp/.builds/archlinux.yml new file mode 100644 index 00000000..1d717ba2 --- /dev/null +++ b/3rdparty/xdgpp/.builds/archlinux.yml @@ -0,0 +1,14 @@ +image: archlinux +packages: + - gcc + - clang + - catch2 +sources: + - https://git.sr.ht/~danyspin97/xdgpp +tasks: + - test-gcc: | + cd xdgpp + CXX=g++ make clean && make test -j2 + - test-clang: | + cd xdgpp + CXX=clang++ make clean && make test -j2 diff --git a/3rdparty/xdgpp/.clang-format b/3rdparty/xdgpp/.clang-format new file mode 100644 index 00000000..cc17704a --- /dev/null +++ b/3rdparty/xdgpp/.clang-format @@ -0,0 +1,5 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -4 +IndentWidth: 4 diff --git a/3rdparty/xdgpp/.clang-tidy b/3rdparty/xdgpp/.clang-tidy new file mode 100644 index 00000000..2b1535c4 --- /dev/null +++ b/3rdparty/xdgpp/.clang-tidy @@ -0,0 +1,37 @@ +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*,misc-*,modernize-*,performance-*,readability-*,portability-*,cppcoreguidelines-*,bugprone-*,hicpp-*' +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: none +CheckOptions: + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: '0' + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: '1' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: '1' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: llvm-qualified-auto.AddConstToQualified + value: '0' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-pass-by-value.IncludeStyle + value: google + - key: modernize-replace-auto-ptr.IncludeStyle + value: google + - key: modernize-use-nullptr.NullMacros + value: 'NULL' +... + diff --git a/3rdparty/xdgpp/.gitignore b/3rdparty/xdgpp/.gitignore new file mode 100644 index 00000000..3c78355e --- /dev/null +++ b/3rdparty/xdgpp/.gitignore @@ -0,0 +1,2 @@ +*.o +xdg_test diff --git a/3rdparty/xdgpp/LICENSE b/3rdparty/xdgpp/LICENSE new file mode 100644 index 00000000..6aec5963 --- /dev/null +++ b/3rdparty/xdgpp/LICENSE @@ -0,0 +1,20 @@ +Copyright © 2020 Danilo Spinella + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/3rdparty/xdgpp/Makefile b/3rdparty/xdgpp/Makefile new file mode 100644 index 00000000..24468ea2 --- /dev/null +++ b/3rdparty/xdgpp/Makefile @@ -0,0 +1,21 @@ +CXX?=g++ +CXXFLAGS?= +DEFAULT_CXXFLAGS:=-std=c++17 -Werror -Wall +CXXFLAGS+=$(DEFAULT_CXXFLAGS) +CATCH2_CXXFLAGS:=-DCATCH_CONFIG_MAIN -DCATCH_CONFIG_FAST_COMPILE +CXXFLAGS+=$(CATCH2_CXXFLAGS) +INCLUDES:=-I. +NAME:=xdg_test + +SOURCES=xdg_test.cpp +OBJ=$(SOURCES:.cpp=.o) + +xdg_test: $(OBJ) + $(CXX) $(CXXFLAGS) $(INCLUDES) $^ -o $(NAME) + +test: xdg_test + ./xdg_test + +.PHONY: clean +clean: + rm -f $(OBJ) $(NAME) diff --git a/3rdparty/xdgpp/README.md b/3rdparty/xdgpp/README.md new file mode 100644 index 00000000..5ef7c294 --- /dev/null +++ b/3rdparty/xdgpp/README.md @@ -0,0 +1,70 @@ +# xdgpp + +[![builds.sr.ht status](https://builds.sr.ht/~danyspin97/xdgpp.svg)](https://builds.sr.ht/~danyspin97/xdgpp?) +![Liberapay receiving](https://img.shields.io/liberapay/receives/danyspin97?logo=liberapay) + +C++17 header-only implementation of the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). + +## Installation + +Copy `xdg.hpp` into your source tree. + +## Usage + +```cpp +#include + +#include "xdg.hpp" + +int main() { + std::cout << "$XDG_DATA_HOME = " << xdg::DataHomeDir() << std::endl; + std::cout << "$XDG_CONFIG_HOME = " << xdg::ConfigHomeDir() << std::endl; + std::cout << "$XDG_CACHE_HOME = " << xdg::CacheHomeDir() << std::endl; + + auto data_dirs = xdg::DataDirs(); + std::cout << "$XDG_DATA_DIRS = \"" << data_dirs.front().c_str(); + for (int i = 1; i < data_dirs.size(); i++) { + std::cout << ":" << data_dirs.at(i).c_str(); + } + std::cout << "\"" << std::endl; + + auto config_dirs = xdg::ConfigDirs(); + std::cout << "$XDG_CONFIG_DIRS = \"" << config_dirs.front().c_str(); + for (int i = 1; i < config_dirs.size(); i++) { + std::cout << ":" << config_dirs.at(i).c_str(); + } + std::cout << "\"" << std::endl; + + // XDG_RUNTIME_DIR might not be set, the API returns a std::optional + auto runtime_dir = xdg::RuntimeDir(); + if (runtime_dir) { + std::cout << "$XDG_RUNTIME_DIR = " << runtime_dir.value(); + } + + return 0; +} +``` + +Alternatively you can create and use an instance of `xdg::BaseDirectories` like: + +```cpp +#include + +#include "xdg.hpp" + +int main() { + xdg::BaseDirectories dirs; + std::cout << "$XDG_DATA_HOME = " << dirs.DataHome() << std::endl; +} +``` + +## Contributing + +Pull requests are welcome. + +**xdgpp** follows the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html); run `clang-format` on changes to automatically format the code. + +## License + +**xdgpp** is licensed under [the Mit License](https://mit-license.org/). + diff --git a/3rdparty/xdgpp/xdg.hpp b/3rdparty/xdgpp/xdg.hpp new file mode 100644 index 00000000..589c034d --- /dev/null +++ b/3rdparty/xdgpp/xdg.hpp @@ -0,0 +1,307 @@ +/* + * Copyright © 2020 Danilo Spinella + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xdg { + +class BaseDirectoryException : public std::exception { +public: + explicit BaseDirectoryException(std::string msg) : msg_(std::move(msg)) {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return msg_.c_str(); + } + [[nodiscard]] auto msg() const noexcept -> std::string { return msg_; } + +private: + const std::string msg_; +}; + +class BaseDirectories { +public: + BaseDirectories() { + const char *home_env = getenv("HOME"); + if (home_env == nullptr) { + throw BaseDirectoryException("$HOME must be set"); + } + home_ = std::filesystem::path{home_env}; + + data_home_ = GetAbsolutePathFromEnvOrDefault( + "XDG_DATA_HOME", home_ / ".local" / "share"); + config_home_ = GetAbsolutePathFromEnvOrDefault("XDG_CONFIG_HOME", + home_ / ".config"); + data_ = GetPathsFromEnvOrDefault("XDG_DATA_DIRS", + std::vector{ + "/usr/local/share", "/usr/share"}); + config_ = GetPathsFromEnvOrDefault( + "XDG_CONFIG_DIRS", std::vector{"/etc/xdg"}); + cache_home_ = + GetAbsolutePathFromEnvOrDefault("XDG_CACHE_HOME", home_ / ".cache"); + // Local extension: StateHomeDir is defined in v0.8 of the XDG Base + // Directory Specification. Default is $HOME/.local/state. + state_home_ = GetAbsolutePathFromEnvOrDefault( + "XDG_STATE_HOME", home_ / ".local" / "state"); + + SetRuntimeDir(); + } + + static auto GetInstance() -> BaseDirectories & { + static BaseDirectories dirs; + + return dirs; + } + + [[nodiscard]] auto DataHome() -> const std::filesystem::path & { + return data_home_; + } + [[nodiscard]] auto ConfigHome() -> const std::filesystem::path & { + return config_home_; + } + [[nodiscard]] auto Data() -> const std::vector & { + return data_; + } + [[nodiscard]] auto Config() -> const std::vector & { + return config_; + } + [[nodiscard]] auto CacheHome() -> const std::filesystem::path & { + return cache_home_; + } + [[nodiscard]] auto StateHome() -> const std::filesystem::path & { + return state_home_; + } + [[nodiscard]] auto Runtime() + -> const std::optional & { + return runtime_; + } + [[nodiscard]] auto Home() -> const std::filesystem::path & { + return home_; + } + +private: + void SetRuntimeDir() { + const char *runtime_env = getenv("XDG_RUNTIME_DIR"); + if (runtime_env != nullptr) { + std::filesystem::path runtime_dir{runtime_env}; + if (runtime_dir.is_absolute()) { + if (!std::filesystem::exists(runtime_dir)) { + throw BaseDirectoryException( + "$XDG_RUNTIME_DIR must exist on the system"); + } + + auto runtime_dir_perms = + std::filesystem::status(runtime_dir).permissions(); + using perms = std::filesystem::perms; + // Check XDG_RUNTIME_DIR permissions are 0700 + if (((runtime_dir_perms & perms::owner_all) == perms::none) || + ((runtime_dir_perms & perms::group_all) != perms::none) || + ((runtime_dir_perms & perms::others_all) != perms::none)) { + throw BaseDirectoryException( + "$XDG_RUNTIME_DIR must have 0700 as permissions"); + } + runtime_.emplace(runtime_dir); + } + } + } + + static auto + GetAbsolutePathFromEnvOrDefault(const char *env_name, + std::filesystem::path &&default_path) + -> std::filesystem::path { + const char *env_var = getenv(env_name); + if (env_var == nullptr) { + return std::move(default_path); + } + auto path = std::filesystem::path{env_var}; + if (!path.is_absolute()) { + return std::move(default_path); + } + + return path; + } + + static auto + GetPathsFromEnvOrDefault(const char *env_name, + std::vector &&default_paths) + -> std::vector { + auto *env = getenv(env_name); + if (env == nullptr) { + return std::move(default_paths); + } + std::string paths{env}; + + std::vector dirs{}; + size_t start = 0; + size_t pos = 0; + while ((pos = paths.find_first_of(':', start)) != std::string::npos) { + std::filesystem::path current_path{ + paths.substr(start, pos - start)}; + if (current_path.is_absolute() && + !VectorContainsPath(dirs, current_path)) { + dirs.emplace_back(current_path); + } + start = pos + 1; + } + std::filesystem::path current_path{paths.substr(start, pos - start)}; + if (current_path.is_absolute() && + !VectorContainsPath(dirs, current_path)) { + dirs.emplace_back(current_path); + } + + if (dirs.empty()) { + return std::move(default_paths); + } + + return dirs; + } + + static auto + VectorContainsPath(const std::vector &paths, + const std::filesystem::path &path) -> bool { + return std::find(std::begin(paths), std::end(paths), path) != + paths.end(); + } + + std::filesystem::path home_; + + std::filesystem::path data_home_; + std::filesystem::path config_home_; + std::vector data_; + std::vector config_; + std::filesystem::path cache_home_; + std::filesystem::path state_home_; + std::optional runtime_; + +}; // namespace xdg + +[[nodiscard]] inline auto DataHomeDir() -> const std::filesystem::path & { + return BaseDirectories::GetInstance().DataHome(); +} +[[nodiscard]] inline auto ConfigHomeDir() -> const std::filesystem::path & { + return BaseDirectories::GetInstance().ConfigHome(); +} +[[nodiscard]] inline auto DataDirs() + -> const std::vector & { + return BaseDirectories::GetInstance().Data(); +} +[[nodiscard]] inline auto ConfigDirs() + -> const std::vector & { + return BaseDirectories::GetInstance().Config(); +} +[[nodiscard]] inline auto CacheHomeDir() -> const std::filesystem::path & { + return BaseDirectories::GetInstance().CacheHome(); +} +// Local extension: per XDG Base Directory Specification v0.8. +[[nodiscard]] inline auto StateHomeDir() -> const std::filesystem::path & { + return BaseDirectories::GetInstance().StateHome(); +} +[[nodiscard]] inline auto RuntimeDir() + -> const std::optional & { + return BaseDirectories::GetInstance().Runtime(); +} + +namespace detail { + +// Pure: strip leading/trailing ASCII whitespace. View-in, view-out. +inline auto trim(std::string_view s) -> std::string_view { + const auto space = [](char c) { + return std::isspace(static_cast(c)); + }; + while (!s.empty() && space(s.front())) s.remove_prefix(1); + while (!s.empty() && space(s.back())) s.remove_suffix(1); + return s; +} + +// Pure: strip a matched pair of surrounding double quotes. No-op otherwise. +inline auto unquote(std::string_view s) -> std::string_view { + if (s.size() >= 2 && s.front() == '"' && s.back() == '"') { + s.remove_prefix(1); + s.remove_suffix(1); + } + return s; +} + +// Pure: expand a leading "$HOME" or "$HOME/" — the only substitution that +// xdg-user-dirs-update emits into user-dirs.dirs. +inline auto expandHome(std::string_view v, const std::string &home) + -> std::string { + if (v == "$HOME") return home; + if (v.substr(0, 6) == "$HOME/") return home + std::string{v.substr(5)}; + return std::string{v}; +} + +// Pure: one line of user-dirs.dirs + the key to match + $HOME → resolved path, +// or nullopt if the line is a comment/blank, doesn't assign `key`, or has an +// empty value. +inline auto parseUserDirLine(std::string_view line, + std::string_view key, + const std::string &home) + -> std::optional { + line = trim(line); + if (line.empty() || line.front() == '#') return std::nullopt; + const auto eq = line.find('='); + if (eq == std::string_view::npos) return std::nullopt; + if (trim(line.substr(0, eq)) != key) return std::nullopt; + const auto value = unquote(trim(line.substr(eq + 1))); + if (value.empty()) return std::nullopt; + return std::filesystem::path{expandHome(value, home)}; +} + +} // namespace detail + +// Local extension: parse $XDG_CONFIG_HOME/user-dirs.dirs (xdg-user-dirs spec) +// and return the value for `key` (e.g. "XDG_PICTURES_DIR"). Expands a leading +// $HOME; strips surrounding double quotes; skips comments and malformed lines. +// Returns nullopt if the file doesn't exist, the key isn't present, or the +// value would be empty. Callers typically layer a per-key default on top. +[[nodiscard]] inline auto UserDir(std::string_view key) + -> std::optional { + const auto cfg = BaseDirectories::GetInstance().ConfigHome() / "user-dirs.dirs"; + std::ifstream file(cfg); + if (!file) return std::nullopt; + const std::string &home = BaseDirectories::GetInstance().Home().string(); + std::string line; + while (std::getline(file, line)) { + if (auto hit = detail::parseUserDirLine(line, key, home)) return hit; + } + return std::nullopt; +} + +// Local extension: Pictures directory per xdg-user-dirs. Falls back to +// $HOME/Pictures if the user-dirs config is missing or unset. +[[nodiscard]] inline auto PicturesDir() -> std::filesystem::path { + if (auto p = UserDir("XDG_PICTURES_DIR")) return *p; + return BaseDirectories::GetInstance().Home() / "Pictures"; +} + +} // namespace xdg diff --git a/3rdparty/xdgpp/xdg_test.cpp b/3rdparty/xdgpp/xdg_test.cpp new file mode 100644 index 00000000..6dfffdb4 --- /dev/null +++ b/3rdparty/xdgpp/xdg_test.cpp @@ -0,0 +1,137 @@ +/* + * Copyright © 2020 Danilo Spinella + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "xdg.hpp" + +#include "catch2/catch.hpp" + +#include + +template +void TestSingleDirectory(T function, const char *env_name, + const std::filesystem::path &default_path) { + unsetenv(env_name); + xdg::BaseDirectories dirs; + CHECK(std::bind(function, &dirs)() == default_path); + + const auto *new_env = "/tmp/mydir"; + setenv(env_name, new_env, 1); + dirs = xdg::BaseDirectories{}; + CHECK(std::bind(function, &dirs)() == std::filesystem::path{new_env}); + + setenv(env_name, &new_env[1], 1); + dirs = xdg::BaseDirectories{}; + CHECK(std::bind(function, &dirs)() == default_path); +} + +template +void TestMultipleDirectories( + T function, const char *env_name, + const std::vector &default_paths) { + unsetenv(env_name); + xdg::BaseDirectories dirs; + CHECK(std::bind(function, &dirs)() == default_paths); + + const auto *new_env = "/tmp/mydir:/tmp/mydir2"; + setenv(env_name, new_env, 1); + dirs = xdg::BaseDirectories{}; + auto paths = + std::vector{"/tmp/mydir", "/tmp/mydir2"}; + CHECK(std::bind(function, &dirs)() == paths); + + new_env = "tmp/mydir:tmp/mydir1"; + setenv(env_name, new_env, 1); + dirs = xdg::BaseDirectories{}; + CHECK(std::bind(function, &dirs)() == default_paths); + + new_env = "/tmp/mydir:tmp/mydir1"; + setenv(env_name, new_env, 1); + dirs = xdg::BaseDirectories{}; + CHECK(std::bind(function, &dirs)() == + std::vector{ + std::filesystem::path{"/tmp/mydir"}}); +} + +TEST_CASE("xdg::Dirs") { + SECTION("XDG_DATA_HOME") { + TestSingleDirectory(&xdg::BaseDirectories::DataHome, "XDG_DATA_HOME", + std::filesystem::path{getenv("HOME")} / ".local" / + "share"); + } + SECTION("XDG_CONFIG_HOME") { + TestSingleDirectory(&xdg::BaseDirectories::ConfigHome, + "XDG_CONFIG_HOME", + std::filesystem::path{getenv("HOME")} / ".config"); + } + SECTION("XDG_DATA_DIRS") { + TestMultipleDirectories(&xdg::BaseDirectories::Data, "XDG_DATA_DIRS", + {std::filesystem::path{"/usr/local/share"}, + std::filesystem::path{"/usr/share"}}); + } + + SECTION("XDG_CONFIG_DIRS") { + TestMultipleDirectories(&xdg::BaseDirectories::Config, + "XDG_CONFIG_DIRS", + {std::filesystem::path{"/etc/xdg"}}); + } + SECTION("XDG_CACHE_HOME") { + TestSingleDirectory(&xdg::BaseDirectories::CacheHome, "XDG_CACHE_HOME", + std::filesystem::path{getenv("HOME")} / ".cache"); + } + + SECTION("XDG_RUNTIME_DIR") { + std::filesystem::path new_dir{std::filesystem::current_path()}; + new_dir /= "runtime_dir"; + if (std::filesystem::exists(new_dir)) { + // Clean up previous tests + std::filesystem::remove(new_dir); + } + setenv("XDG_RUNTIME_DIR", new_dir.c_str(), 1); + CHECK_THROWS_AS(xdg::BaseDirectories{}, xdg::BaseDirectoryException); + + // Create directory + std::filesystem::create_directory(new_dir); + // Apply 0777 mode + std::filesystem::permissions(new_dir, + std::filesystem::perms::owner_all | + std::filesystem::perms::group_all + + | std::filesystem::perms::others_all, + std::filesystem::perm_options::add); + REQUIRE_THROWS_AS(xdg::BaseDirectories{}, xdg::BaseDirectoryException); + + // Remove group_all and other_all, so that permissions will be 0700 + std::filesystem::permissions(new_dir, + std::filesystem::perms::group_all + + | std::filesystem::perms::others_all, + std::filesystem::perm_options::remove); + + xdg::BaseDirectories *dirs = nullptr; + REQUIRE_NOTHROW(dirs = new xdg::BaseDirectories{}); + REQUIRE(dirs->Runtime()); + CHECK(dirs->Runtime().value() == new_dir); + + std::filesystem::remove(new_dir); + delete dirs; + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt old mode 100755 new mode 100644 index 713fa4f8..80db86da --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,24 +1,28 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.22...3.28) #compiler settings -#set(CMAKE_CXX_STANDARD 11) -#set(CMAKE_CXX_STANDARD_REQUIRED ON) -#set(CMAKE_CXX_EXTENSIONS OFF) -#set(CMAKE_C_COMPILER gcc) -#set(CMAKE_CXX_COMPILER g++) +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # common set(PROJECT_NAME "xpeccy") project(${PROJECT_NAME}) +option(USEOPENGL "Enable OpenGL-accelerated rendering" ON) +option(USEQTNETWORK "Enable Qt network support" OFF) +set(QTVERSION "" CACHE STRING "Force Qt version (5 or 6). Empty = auto-detect.") + file(STRINGS "Release" XVER) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) include(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake) -# endianess +# endianness include (TestBigEndian) test_big_endian(BIG_ENDIAN) @@ -30,12 +34,12 @@ endif() # compilation flags -set(CMAKE_C_FLAGS "-std=gnu99 -Wall -O2 ${CMAKE_C_FLAGS}") -set(CMAKE_C_FLAGS_RELEASE "-DNDEBUG ${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS "-Wall ${CMAKE_C_FLAGS}") +set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG ${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_DEBUG "-g -DISDEBUG ${CMAKE_C_FLAGS_DEBUG}") -set(CMAKE_CXX_FLAGS "-std=c++11 -Wall -O2 ${CMAKE_CXX_FLAGS}") -set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG ${CMAKE_CXX_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS "-Wall ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG ${CMAKE_CXX_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_DEBUG "-g -DISDEBUG ${CMAKE_CXX_FLAGS_DEBUG}") # OS-depended section @@ -77,10 +81,22 @@ if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") install(FILES ${CMAKE_BINARY_DIR}/${PROJECT_NAME} DESTINATION bin PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE WORLD_READ WORLD_EXECUTE GROUP_READ GROUP_EXECUTE ) - install(FILES images/xpeccy.png DESTINATION share/icons) + # Install the icon per the legacy pixmaps fallback documented in Icon + # Theme Spec §2.1 — universally supported by desktop icon lookup even on + # systems without a configured hicolor theme. (The source icon is 126x126 + # which is awkward to slot into hicolor/x/apps; pixmaps is the + # cleaner fit until a multi-size icon set ships.) + install(FILES images/xpeccy.png DESTINATION share/pixmaps) install(FILES Xpeccy.desktop DESTINATION share/applications) + # Ship bundled palettes under the XDG Data readonly search path. The + # framework resolves ResourceKind::Palette via $XDG_DATA_DIRS entries + + # samstyle/xpeccy/palettes, so files installed here appear as system + # defaults alongside any user copies in $XDG_DATA_HOME. + install(DIRECTORY conf/palettes/ + DESTINATION share/samstyle/xpeccy/palettes + FILES_MATCHING PATTERN "*.txt" PATTERN "*.pal") -elseif(${CMAKE_SYSTEM_NAME} MATCHES "/BSD$/") +elseif(${CMAKE_SYSTEM_NAME} MATCHES "BSD") set(INC_PATHS pkg/include include) set(LIB_PATHS lib lib64 pkg/lib local/lib64) @@ -179,131 +195,103 @@ set(MOCFILES ./src/xgui/labelist.h ) -# Qt5 / Qt6 +# Qt version detection — prefer Qt6, fall back to Qt5. +# Override with -DQTVERSION=5 or -DQTVERSION=6 (or legacy -DQT6BUILD=1/0). -if (DEFINED QT6BUILD) - if (${QT6BUILD}) - set(QTVERSION 6) - else(${QT6BUILD}) - set(QTVERSION 5) - endif(${QT6BUILD}) -elseif(NOT DEFINED QTVERSION) - set(QTVERSION 5) +if(DEFINED QT6BUILD) + if(QT6BUILD) + set(QTVERSION 6 CACHE STRING "" FORCE) + else() + set(QTVERSION 5 CACHE STRING "" FORCE) + endif() endif() -if(${QTVERSION} EQUAL 4) - find_package(Qt4 COMPONENTS QtCore QtGui REQUIRED) - include(${QT_USE_FILE}) - qt4_add_resources(RESOURCES ${QRCFILES}) - qt4_wrap_ui(UIHEADERS ${UIFILES}) - qt4_wrap_cpp(MOCHEADERS ${MOCFILES}) - set(CPACK_RPM_PACKAGE_REQUIRES "libqt4 >= 4.6, libqt4-x11 >= 4.6") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqtcore4 (>=4.6), libqtgui4 (>=4.6)") - - if(${USEQTNETWORK}) - find_package(Qt4 COMPONENTS QtNetwork REQUIRED) - set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libqt4-network >= 4.6") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt4-network (>=4.6)") - add_definitions("-DUSENETWORK") - endif() - if(${USEOPENGL}) - find_package(Qt4 COMPONENTS QtOpenGL REQUIRED) - set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libqt4-opengl >= 4.6") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt4-opengl (>=4.6)") - add_definitions("-DUSEOPENGL") - endif() +if(QTVERSION STREQUAL "") + find_package(QT NAMES Qt6 Qt5 REQUIRED) +else() + find_package(QT NAMES Qt${QTVERSION} REQUIRED) +endif() - set(LIBRARIES ${LIBRARIES} ${QT_LIBRARIES}) - set(QT_VER ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}) - -elseif(${QTVERSION} EQUAL 5) - - find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) - set(LIBRARIES ${LIBRARIES} Qt5::Widgets) - qt5_add_resources(RESOURCES ${QRCFILES}) - qt5_wrap_ui(UIHEADERS ${UIFILES}) - qt5_wrap_cpp(MOCHEADERS ${MOCFILES}) - set(CPACK_RPM_PACKAGE_REQUIRES "libQt5Gui5 >= 5.3, libQt5Widgets5 >= 5.3") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5gui5 (>=5.3), libqt5widgets5 (>=5.3)") - - if(${USEQTNETWORK}) - find_package(Qt5 COMPONENTS Network REQUIRED) - set(LIBRARIES ${LIBRARIES} Qt5::Network) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt5network5 (>=5.3)") - set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt5Network5 >= 5.3") - add_definitions("-DUSENETWORK") - endif() - if (${USEOPENGL}) - find_package(OpenGL REQUIRED) - find_package(Qt5 COMPONENTS OpenGL REQUIRED) - set(LIBRARIES ${LIBRARIES} Qt5::OpenGL ${OPENGL_LIBRARIES}) - set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt5OpenGL5 >= 5.3") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt5opengl5 (>=5.3)") - add_definitions("-DUSEOPENGL") +set(_QTV ${QT_VERSION_MAJOR}) +if(_QTV EQUAL 6) + set(_QT_MIN_VER 6.1) +else() + set(_QT_MIN_VER 5.15) +endif() + +# Core Qt components +find_package(Qt${_QTV} ${_QT_MIN_VER} REQUIRED COMPONENTS Core Gui Widgets) +set(LIBRARIES ${LIBRARIES} Qt${_QTV}::Widgets) + +if(_QTV EQUAL 6) + find_package(Qt6 REQUIRED COMPONENTS Core5Compat) + set(LIBRARIES ${LIBRARIES} Qt6::Core Qt6::Gui Qt6::Core5Compat) +endif() + +cmake_language(CALL qt${_QTV}_add_resources RESOURCES ${QRCFILES}) +cmake_language(CALL qt${_QTV}_wrap_ui UIHEADERS ${UIFILES}) +cmake_language(CALL qt${_QTV}_wrap_cpp MOCHEADERS ${MOCFILES}) + +# Optional: network +if(USEQTNETWORK) + find_package(Qt${_QTV} REQUIRED COMPONENTS Network) + set(LIBRARIES ${LIBRARIES} Qt${_QTV}::Network) + add_definitions(-DUSENETWORK) +endif() + +# Optional: OpenGL +if(USEOPENGL) + find_package(OpenGL REQUIRED) + if(_QTV EQUAL 6) + set(_QT_GL OpenGLWidgets) + else() + set(_QT_GL OpenGL) endif() + find_package(Qt${_QTV} REQUIRED COMPONENTS ${_QT_GL}) + set(LIBRARIES ${LIBRARIES} Qt${_QTV}::${_QT_GL} ${OPENGL_LIBRARIES}) + add_definitions(-DUSEOPENGL) +endif() - set(QT_VER ${Qt5Widgets_VERSION}) - -elseif(${QTVERSION} EQUAL 6) -# message(FATAL_ERROR "Qt6 usage is not implemented yet") -# Qt6 requires c++17 - set(CMAKE_CXX_STANDARD 17) - set(CMAKE_CXX_STANDARD_REQUIRED ON) - - find_package(Qt6 COMPONENTS Core Gui Widgets Core5Compat REQUIRED) - set(LIBRARIES ${LIBRARIES} Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Core5Compat) - qt6_add_resources(RESOURCES ${QRCFILES}) - qt6_wrap_ui(UIHEADERS ${UIFILES}) - qt6_wrap_cpp(MOCHEADERS ${MOCFILES}) - set(CPACK_RPM_PACKAGE_REQUIRES "libQt6Gui6 >= 6.1, libQt6Widgets6 >= 6.1, libQt6Core5Compat6 >= 6.1") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6gui6 (>=6.1), libqt6widgets6 (>=6.1), libqt6core5compat6 >= 6.1") - if(${USEQTNETWORK}) - find_package(Qt6 COMPONENTS Network REQUIRED) - set(LIBRARIES ${LIBRARIES} Qt6::Network) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt6network6 (>=6.1)") - set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt6Network6 >= 6.1") - add_definitions("-DUSENETWORK") +# Packaging dependencies +if(_QTV EQUAL 6) + set(CPACK_RPM_PACKAGE_REQUIRES "libQt6Gui6 >= ${_QT_MIN_VER}, libQt6Widgets6 >= ${_QT_MIN_VER}, libQt6Core5Compat6 >= ${_QT_MIN_VER}") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6gui6 (>=${_QT_MIN_VER}), libqt6widgets6 (>=${_QT_MIN_VER}), libqt6core5compat6 (>=${_QT_MIN_VER})") + if(USEQTNETWORK) + set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt6Network6 >= ${_QT_MIN_VER}") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt6network6 (>=${_QT_MIN_VER})") endif() - if (${USEOPENGL}) - find_package(OpenGL REQUIRED) - find_package(Qt6 COMPONENTS OpenGLWidgets REQUIRED) - set(LIBRARIES ${LIBRARIES} Qt6::OpenGLWidgets ${OPENGL_LIBRARIES}) - set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt6OpenGL6 >= 6.1") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt6opengl6 (>=6.1)") - add_definitions("-DUSEOPENGL") + if(USEOPENGL) + set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt6OpenGL6 >= ${_QT_MIN_VER}") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt6opengl6 (>=${_QT_MIN_VER})") endif() - - set(QT_VER ${Qt6Widgets_VERSION}) else() - message(FATAL_ERROR "Not supported Qt version defined") + set(CPACK_RPM_PACKAGE_REQUIRES "libQt5Gui5 >= ${_QT_MIN_VER}, libQt5Widgets5 >= ${_QT_MIN_VER}") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5gui5 (>=${_QT_MIN_VER}), libqt5widgets5 (>=${_QT_MIN_VER})") + if(USEQTNETWORK) + set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt5Network5 >= ${_QT_MIN_VER}") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt5network5 (>=${_QT_MIN_VER})") + endif() + if(USEOPENGL) + set(CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES}, libQt5OpenGL5 >= ${_QT_MIN_VER}") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libqt5opengl5 (>=${_QT_MIN_VER})") + endif() endif() +set(QT_VER ${QT_VERSION}) + # SDL -if(${SDL1BUILD}) - find_package(SDL REQUIRED) - if(${SDL_FOUND}) - add_definitions(-DHAVESDL1) - include_directories(${SDL_INCLUDE_DIR}) - set(LIBRARIES ${LIBRARIES} ${SDL_LIBRARY}) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libSDL1.2debian (>=1.2)") - set(CPACK_RPM_PACKAGE_DEPENDS "${CPACK_RPM_PACKAGE_DEPENDS}, SDL >= 1.2") - set(SDL_VER ${SDL_VERSION_STRING}) - set(SDL_NAME "SDL") - endif(${SDL_FOUND}) -else(${SDL1BUILD}) - find_package(SDL2 REQUIRED) - if (${SDL2_FOUND}) - add_definitions(-DHAVESDL2) - include_directories(${SDL2_INCLUDE_DIR}) - set(LIBRARIES ${LIBRARIES} ${SDL2_LIBRARY}) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libsdl2-2.0-0 (>=2.0) | libSDL2 (>=2.0)") - set(CPACK_RPM_PACKAGE_DEPENDS "${CPACK_RPM_PACKAGE_DEPENDS}, SDL2 >= 2.0") - set(SDL_VER "2.x") - set(SDL_NAME "SDL2") - message(STATUS "SDL2_INCLUDE_DIR = " ${SDL2_INCLUDE_DIR}) - endif(${SDL2_FOUND}) -endif(${SDL1BUILD}) +find_package(SDL2 REQUIRED) +if (${SDL2_FOUND}) + add_definitions(-DHAVESDL2) + include_directories(${SDL2_INCLUDE_DIR}) + set(LIBRARIES ${LIBRARIES} ${SDL2_LIBRARY}) + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS}, libsdl2-2.0-0 (>=2.0) | libSDL2 (>=2.0)") + set(CPACK_RPM_PACKAGE_DEPENDS "${CPACK_RPM_PACKAGE_DEPENDS}, SDL2 >= 2.0") + set(SDL_VER "2.x") + set(SDL_NAME "SDL2") + message(STATUS "SDL2_INCLUDE_DIR = " ${SDL2_INCLUDE_DIR}) +endif(${SDL2_FOUND}) # zlib (for rzx) @@ -316,20 +304,16 @@ if(ZLIB_FOUND) set(CPACK_RPM_PACKAGE_DEPENDS "${CPACK_RPM_PACKAGE_DEPENDS}, zlib >= 1.2") endif(ZLIB_FOUND) -# flags +# vendored dependencies -if(${IBM}) - add_definitions(-DUSEIBM=1) -endif() +include_directories(${PROJECT_SOURCE_DIR}/3rdparty/xdgpp) -# other +# executable -if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") - add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS} ${UIHEADERS} ${RESOURCES} ${MOCHEADERS}) -elseif(${CMAKE_SYSTEM_NAME} MATCHES "/BSD$/") - add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS} ${UIHEADERS} ${RESOURCES} ${MOCHEADERS}) -elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${SOURCES} ${HEADERS} ${UIHEADERS} ${RESOURCES} ${MOCHEADERS} ${BUNDLE_ICON_PATH}) +set(ALL_SRCS ${SOURCES} ${HEADERS} ${UIHEADERS} ${RESOURCES} ${MOCHEADERS}) + +if(APPLE) + add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${ALL_SRCS} ${BUNDLE_ICON_PATH}) find_program(MACDEPLOYQTEXE macdeployqt) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${MACDEPLOYQTEXE} ARGS ${BUNDLE_PATH} -always-overwrite @@ -339,8 +323,10 @@ elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") ) set(CMAKE_INSTALL_PREFIX "/Applications") install(TARGETS ${PROJECT_NAME} BUNDLE DESTINATION .) -elseif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - add_executable(${PROJECT_NAME} WIN32 ${SOURCES} ${HEADERS} ${UIHEADERS} ${RESOURCES} ${MOCHEADERS} ${CMAKE_SOURCE_DIR}/xpeccy.rc) +elseif(WIN32) + add_executable(${PROJECT_NAME} WIN32 ${ALL_SRCS} ${CMAKE_SOURCE_DIR}/xpeccy.rc) +else() + add_executable(${PROJECT_NAME} ${ALL_SRCS}) endif() include_directories(${INCLUDIRS}) diff --git a/Xpeccy.desktop b/Xpeccy.desktop index 61c152b2..8dd6a6bc 100644 --- a/Xpeccy.desktop +++ b/Xpeccy.desktop @@ -4,9 +4,10 @@ Name=Xpeccy Name[ru]=Xpeccy Comment=ZX Spectrum emulator Comment[ru]=Эмулятор ZX Spectrum -Exec=xpeccy +Exec=xpeccy %f Icon=xpeccy Type=Application Terminal=false StartupNotify=true -Categories=Game;Emulator; \ No newline at end of file +Categories=Game;Emulator; +Keywords=Spectrum;Sinclair;ZX;emulator;retro;8-bit; \ No newline at end of file diff --git a/src/emulwin.cpp b/src/emulwin.cpp index c61e3827..64c5029d 100644 --- a/src/emulwin.cpp +++ b/src/emulwin.cpp @@ -1,7 +1,9 @@ +#include #include #include #include #include +#include #include #include #include @@ -23,6 +25,7 @@ #include "xcore/xcore.h" #include "xcore/sound.h" +#include "xgui/resources_ui.h" #include "emulwin.h" #include "filer.h" #include "watcher.h" @@ -200,9 +203,7 @@ MainWin::MainWin() { cmsid = startTimer(1000); // 1 sec connect(&frm_tmr, SIGNAL(timeout()), this, SLOT(frame_timer())); -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) frm_tmr.setTimerType(Qt::PreciseTimer); -#endif frm_tmr.start(20); connect(userMenu,SIGNAL(aboutToShow()),SLOT(menuShow())); @@ -214,27 +215,11 @@ MainWin::MainWin() { connect(&srv, SIGNAL(newConnection()),this, SLOT(connected())); #endif -// a legacygl code must be here, else the main process doesn't finish properly (???) -#if USELEGACYGL - QGLFormat frmt; - frmt.setDoubleBuffer(false); - cont = new QGLContext(frmt); - setContext(cont); - setAutoBufferSwap(true); - makeCurrent(); - curtex = 0; - shd_support = QGLShader::hasOpenGLShaders(QGLShader::Vertex) && QGLShader::hasOpenGLShaders(QGLShader::Fragment); - qDebug() << "vtx_shd"; - vtx_shd = new QGLShader(QGLShader::Vertex, cont); - qDebug() << "frg_shd"; - frg_shd = new QGLShader(QGLShader::Fragment, cont); -#endif - qDebug() << "end:constructor"; } MainWin::~MainWin() { -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL cleanupGL(); delete(vtx_shd); delete(frg_shd); @@ -634,7 +619,7 @@ void MainWin::frame_timer() { frm_tmr.setInterval(frm_ns / 1000000); // 1e6 ns = 1 ms. next frame shot frm_ns = frm_ns % 1000000; // remains } -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL if (conf.emu.fast || conf.emu.pause) { glBindTexture(GL_TEXTURE_2D, texids[curtex]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bytesPerLine / 4, comp->vid->vsze.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, comp->flgDBG ? scrimg : bufimg); @@ -651,7 +636,7 @@ void MainWin::frame_timer() { void MainWin::d_frame() { if (conf.emu.fast) return; -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL Computer* comp = conf.prof.cur->zx; queue.append(texids[curtex]); if (queue.size() > 3) @@ -678,7 +663,7 @@ void MainWin::drawText(QPainter* pnt, int x, int y, const char* buf) { void MainWin::paintEvent(QPaintEvent*) { QPainter pnt(this); -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL pnt.beginNativePainting(); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -1073,17 +1058,9 @@ void MainWin::fillUserMenu() { act->setCheckable(true); if (conf.prof.cur) { if (conf.prof.cur->kmapName.empty()) act->setChecked(true); - QDir dir(conf.path.confDir.c_str()); - QStringList lst = dir.entryList(QStringList() << "*.map",QDir::Files,QDir::Name); - dir.setPath(dir.path().append("/keymaps/")); - lst.append(dir.entryList(QStringList() << "*.map",QDir::Files,QDir::Name)); - lst.sort(); - foreach(QString str, lst) { - act = keyMenu->addAction(str); - act->setData(str); - act->setCheckable(true); - act->setChecked(conf.prof.cur->kmapName == std::string(str.toUtf8().data())); - } + fillCheckableMenuFromResources(keyMenu, conf.path.keymap, + byExtension({".map"}), + toQString(conf.prof.cur->kmapName)); } // fill shader menu shdMenu->clear(); @@ -1091,16 +1068,11 @@ void MainWin::fillUserMenu() { act->setData(""); act->setCheckable(true); if (conf.vid.shader.empty()) act->setChecked(true); -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL if (conf.vid.shd_support) { - QDir dir(conf.path.shdDir.c_str()); - QFileInfoList lst = dir.entryInfoList(QStringList() << "*.txt", QDir::Files, QDir::Name); - foreach(QFileInfo inf, lst) { - act = shdMenu->addAction(inf.fileName()); - act->setData(inf.fileName()); - act->setCheckable(true); - act->setChecked(inf.fileName() == conf.vid.shader.c_str()); - } + fillCheckableMenuFromResources(shdMenu, conf.path.shader, + byExtension({".txt"}), + toQString(conf.vid.shader)); } #endif // fill palette menu @@ -1109,14 +1081,9 @@ void MainWin::fillUserMenu() { act->setData(""); act->setCheckable(true); if (conf.prof.cur->palette.empty()) act->setChecked(true); - QDir dir(conf.path.palDir.c_str()); - QFileInfoList lst = dir.entryInfoList(QStringList() << "*.txt", QDir::Files, QDir::Name); - foreach(QFileInfo inf, lst) { - act = palMenu->addAction(inf.fileName()); - act->setData(inf.fileName()); - act->setCheckable(true); - act->setChecked(inf.fileName() == conf.prof.cur->palette.c_str()); - } + fillCheckableMenuFromResources(palMenu, conf.path.palette, + byExtension({".txt"}), + toQString(conf.prof.cur->palette)); } // SLOTS @@ -1140,7 +1107,7 @@ void MainWin::optApply() { } #endif -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL loadShader(); #endif emit s_tape_upd(comp->tape); diff --git a/src/emulwin.h b/src/emulwin.h index 7fb6cb08..f152ae6c 100644 --- a/src/emulwin.h +++ b/src/emulwin.h @@ -28,13 +28,7 @@ #define STICKY_KEY 1 inline qreal widgetDpr(const QWidget* w) { -#if QT_VERSION >= QT_VERSION_CHECK(5,6,0) return w->devicePixelRatioF(); -#elif QT_VERSION >= QT_VERSION_CHECK(5,0,0) - return w->devicePixelRatio(); -#else - return 1.0; -#endif } enum { @@ -56,23 +50,12 @@ typedef struct { QString imgName; } xLed; -// QOpenGLWidget since Qt5.4 - -#define BLOCKGL 0 -#define USELEGACYGL 0 -#define ISLEGACYGL ((QT_VERSION < QT_VERSION_CHECK(5,4,0)) || (USELEGACYGL && (QT_VERSION < QT_VERSION_CHECK(6,0,0)))) - #ifdef USEOPENGL #include - - #if ISLEGACYGL - class MainWin : public QGLWidget { - #else - #include - #include - #include - class MainWin : public QOpenGLWidget, protected QOpenGLFunctions { - #endif + #include + #include + #include + class MainWin : public QOpenGLWidget, protected QOpenGLFunctions { #else class MainWin : public QWidget { #endif @@ -225,7 +208,7 @@ typedef struct { void focusInEvent(QFocusEvent*); void timerEvent(QTimerEvent*); void moveEvent(QMoveEvent*); -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL unsigned curtex:2; GLuint texids[4]; GLuint curtxid; @@ -234,17 +217,10 @@ typedef struct { void resizeGL(int,int); void paintGL(); void cleanupGL(); -#if ISLEGACYGL - QGLContext* cont; - QGLShaderProgram prg; - QGLShader* vtx_shd; - QGLShader* frg_shd; -#else QOpenGLShaderProgram prg; QOpenGLShader* vtx_shd; QOpenGLShader* frg_shd; QOpenGLVertexArrayObject vao; QOpenGLBuffer vbo; #endif -#endif }; diff --git a/src/emw_opengl.cpp b/src/emw_opengl.cpp index b4142899..b5238c19 100644 --- a/src/emw_opengl.cpp +++ b/src/emw_opengl.cpp @@ -4,7 +4,7 @@ #include #include -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL namespace { @@ -174,7 +174,6 @@ QString shimLegacyShader(QString src, void MainWin::initializeGL() { qDebug() << __FUNCTION__; -#if !ISLEGACYGL initializeOpenGLFunctions(); conf.vid.shd_support = QOpenGLShader::hasOpenGLShaders(QOpenGLShader::Vertex) && QOpenGLShader::hasOpenGLShaders(QOpenGLShader::Fragment); curtex = 0; @@ -182,20 +181,6 @@ void MainWin::initializeGL() { vtx_shd = new QOpenGLShader(QOpenGLShader::Vertex); qDebug() << "frg_shd"; frg_shd = new QOpenGLShader(QOpenGLShader::Fragment); -#else -// QGLFormat frmt; -// frmt.setDoubleBuffer(false); -// cont = new QGLContext(frmt); -// setContext(cont); -// setAutoBufferSwap(true); -// makeCurrent(); -// curtex = 0; -// conf.vid.shd_support = QGLShader::hasOpenGLShaders(QGLShader::Vertex) && QGLShader::hasOpenGLShaders(QGLShader::Fragment); -// qDebug() << "vtx_shd"; -// vtx_shd = new QGLShader(QGLShader::Vertex, cont); -// qDebug() << "frg_shd"; -// frg_shd = new QGLShader(QGLShader::Fragment, cont); -#endif glGenTextures(4, texids); glEnable(GL_MULTISAMPLE); for (int i = 0; i < 4; i++) { @@ -206,7 +191,6 @@ void MainWin::initializeGL() { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } -#if !ISLEGACYGL vao.create(); vao.bind(); vbo.create(); @@ -221,7 +205,6 @@ void MainWin::initializeGL() { reinterpret_cast(2 * sizeof(GLfloat))); vbo.release(); vao.release(); -#endif loadShader(); if (!conf.vid.shd_support) qDebug() << "WARNING: Shaders not supported"; @@ -254,7 +237,7 @@ void MainWin::paintGL() { #endif void MainWin::loadShader() { -#if defined(USEOPENGL) && !BLOCKGL +#ifdef USEOPENGL if (!conf.vid.shd_support) return; QString vtx; @@ -262,7 +245,7 @@ void MainWin::loadShader() { bool user_shader = false; if (!conf.vid.shader.empty()) { - QString path(std::string(conf.path.shdDir + SLASH + conf.vid.shader).c_str()); + QString path = toQString(conf.path.shader.find(conf.vid.shader)); QFile file(path); if (file.open(QFile::ReadOnly)) { int mode = 0; diff --git a/src/filer.cpp b/src/filer.cpp index d2ba033a..0932a68f 100644 --- a/src/filer.cpp +++ b/src/filer.cpp @@ -355,8 +355,10 @@ void disk_boot(Computer* comp, int drv, int id) { int idx = 0; while (boot_ft[idx] && (boot_ft[idx] != id)) idx++; - if (boot_ft[idx]) - loadBoot(comp, conf.path.boot.c_str(), drv); + if (boot_ft[idx]) { + if (const auto path = conf.path.boot.tryFind("boot.$B")) + loadBoot(comp, path->string().c_str(), drv); + } } int load_file(Computer* comp, const char* name, int id, int drv) { diff --git a/src/libxpeccy/cpu/LR35902/lr_pref_cb.c b/src/libxpeccy/cpu/LR35902/lr_pref_cb.c index 41962da7..fbbaa8e3 100644 --- a/src/libxpeccy/cpu/LR35902/lr_pref_cb.c +++ b/src/libxpeccy/cpu/LR35902/lr_pref_cb.c @@ -57,7 +57,7 @@ void lrcb2D(CPU* cpu) {SRAX(cpu->regL);} void lrcb2E(CPU* cpu) {cpu->tmpb = lr_mrd(cpu, cpu->regHL); cpu->t++; SRAX(cpu->tmpb); lr_mwr(cpu, cpu->regHL, cpu->tmpb);} void lrcb2F(CPU* cpu) {SRAX(cpu->regA);} // 30..37 swap -inline unsigned char lr_swaph(CPU* cpu, unsigned char v) { +static inline unsigned char lr_swaph(CPU* cpu, unsigned char v) { v = ((v & 0x0f) << 4) | ((v & 0xf0) >> 4); cpu->flgC = 0; cpu->flgN = 0; diff --git a/src/libxpeccy/video/video.c b/src/libxpeccy/video/video.c index 7918651f..d3830d28 100644 --- a/src/libxpeccy/video/video.c +++ b/src/libxpeccy/video/video.c @@ -56,7 +56,7 @@ int vid_visible(Video* vid) { static int32_t outcol; -inline void vid_dot_full(Video* vid, unsigned char idx) { +void vid_dot_full(Video* vid, unsigned char idx) { if (vid->hvis && vid->vvis) { outcol = greyScale ? vid->gpal[idx] : vid->pal[idx]; #if defined(USEOPENGL) @@ -75,7 +75,7 @@ inline void vid_dot_full(Video* vid, unsigned char idx) { } } -inline void vid_dot_half(Video* vid, unsigned char idx) { +void vid_dot_half(Video* vid, unsigned char idx) { if (vid->hvis && vid->vvis) { outcol = greyScale ? vid->gpal[idx] : vid->pal[idx]; #if defined(USEOPENGL) diff --git a/src/main.cpp b/src/main.cpp index 6c1382ff..c787751c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,15 +58,10 @@ void xApp::d_frame() { } void xApp::d_style() { - if (conf.style.empty()) { - setStyleSheet(""); - } else { - std::string path = conf.path.qssDir + SLASH + conf.style; - QFile file(path.c_str()); - if (file.open(QFile::ReadOnly)) { - setStyleSheet(file.readAll().data()); - file.close(); - } + if (conf.style.empty()) { setStyleSheet(""); return; } + QFile file(toQString(conf.path.style.find(conf.style))); + if (file.open(QFile::ReadOnly)) { + setStyleSheet(QString::fromUtf8(file.readAll())); } } @@ -105,7 +100,7 @@ int main(int ac,char** av) { #endif printf("Using Qt ver %s\n",qVersion()); -#if (QT_VERSION >= QT_VERSION_CHECK(5,6,0)) && (QT_VERSION < QT_VERSION_CHECK(6,0,0)) +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif diff --git a/src/xcore/config.cpp b/src/xcore/config.cpp index 4d348d85..3e831e5f 100644 --- a/src/xcore/config.cpp +++ b/src/xcore/config.cpp @@ -1,11 +1,17 @@ #include #include +#include #include +#include +#include +#include #include +#include #include #include "xcore.h" +#include "migrate.h" #include "vscalers.h" #include "../xgui/xgui.h" #include "sound.h" @@ -42,61 +48,188 @@ enum { std::map shotFormat; xConfig conf; +namespace { + +// Which XDG root a resource kind lives under. Per the XDG Base Directory spec: +// Config = settings that customize app behavior (keybindings, input maps). +// Data = assets and large blobs (ROMs, shaders, plugins, palettes, styles). +enum class ResourceBase { Config, Data }; + +struct ResourceSpec { + std::string_view subdir; // relative to the selected base home dir + std::string_view legacySubdir; // subdir under confDir to migrate from; "" = none + ResourceBase base; // Config ($XDG_CONFIG_HOME) or Data ($XDG_DATA_HOME) +}; + +// Pure: build a ResourceDirs value from a spec and the already-resolved XDG +// roots. Picks the Config- or Data-home pair based on spec.base. No filesystem +// access, no logging — side-effect-free, trivially testable in isolation. +ResourceDirs buildResourceDirs(const ResourceSpec &spec, + const fs::path &configHomeDir, + const fs::path &dataHomeDir, + const std::vector &xdgConfigDirBases, + const std::vector &xdgDataDirBases) { + const bool isConfig = spec.base == ResourceBase::Config; + const fs::path &homeDir = isConfig ? configHomeDir : dataHomeDir; + const auto &readonlyBases = isConfig ? xdgConfigDirBases : xdgDataDirBases; + ResourceDirs out; + out.writable = homeDir / spec.subdir; + out.readonly.reserve(readonlyBases.size()); + std::transform(readonlyBases.begin(), readonlyBases.end(), + std::back_inserter(out.readonly), + [&spec](const fs::path &base) { return base / spec.subdir; }); + return out; +} + +// Resolved XDG roots (or their Windows-side equivalents). `conf_init` fills +// one of these per-platform; `applyPlatformRoots` consumes it to populate +// `conf.path` and run migrations. Keeps the platform-specific resolution +// separate from the shared persistence layer. +struct PlatformRoots { + fs::path confHome; // $XDG_CONFIG_HOME/samstyle/xpeccy (or Windows confdir) + fs::path dataHome; // $XDG_DATA_HOME/samstyle/xpeccy (or confHome on Windows) + fs::path cacheHome; // $XDG_CACHE_HOME/samstyle/xpeccy (or confHome/cache) + fs::path stateHome; // $XDG_STATE_HOME/samstyle/xpeccy (or confHome/state) + std::vector configDirs; // $XDG_CONFIG_DIRS bases + suffix (empty on Windows) + std::vector dataDirs; // $XDG_DATA_DIRS bases + suffix (empty on Windows) +}; + +void makeDir(const fs::path &p, std::string_view label) { + std::error_code ec; + fs::create_directories(p, ec); + if (ec) { + std::cout << "create " << label << " failed: " << ec.message() << std::endl; + } +} + +// Populate conf.path.* from `roots`, create every directory, and run one-shot +// legacy migrations from confDir into per-kind subdirs. Idempotent on re-run +// — migrations short-circuit once targets exist. +void applyPlatformRoots(const PlatformRoots &roots) { + auto &p = conf.path; + p.confDir = roots.confHome; + p.prfDir = p.confDir / "profiles"; + p.confFile = p.confDir / "config.conf"; + p.cacheDir = roots.cacheHome; + p.stateDir = roots.stateHome; + p.prfStateDir = p.stateDir / "profiles"; + makeDir(p.confDir, "confDir"); + makeDir(roots.dataHome,"dataHomeDir"); + makeDir(p.prfDir, "prfDir"); + makeDir(p.cacheDir, "cacheDir"); + makeDir(p.stateDir, "stateDir"); + makeDir(p.prfStateDir, "prfStateDir"); + + // Per-kind setup: build the search set, migrate any legacy subdir, mkdir + // the writable target. `label` doubles as the `subdir` for diagnostics + // (identical in practice) and as the "what" argument for migrateDir. + auto initKind = [&](ResourceDirs &out, const ResourceSpec &spec) { + out = buildResourceDirs(spec, roots.confHome, roots.dataHome, + roots.configDirs, roots.dataDirs); + if (!spec.legacySubdir.empty()) { + migrate::migrateDir(p.confDir / spec.legacySubdir, out.writable, + spec.subdir); + } + makeDir(out.writable, spec.subdir); + }; + initKind(p.rom, {"roms", "roms", ResourceBase::Data}); + initKind(p.shader, {"shaders", "shaders", ResourceBase::Data}); + initKind(p.palette, {"palettes", "palettes", ResourceBase::Data}); + initKind(p.pluginCpu, {"plugins/cpu", "plugins/cpu", ResourceBase::Data}); + initKind(p.style, {"styles", "styles", ResourceBase::Data}); + initKind(p.keymap, {"keymaps", "", ResourceBase::Config}); + initKind(p.gamepad, {"gamepads", "", ResourceBase::Config}); + initKind(p.boot, {"boot", "", ResourceBase::Data}); + + // Pull pre-subdir flat files out of confDir into their per-kind subdirs. + // Historically *.map keymaps and *.pad gamepad maps lived flat in confDir; + // boot.$B and debuga.layout were also confDir-rooted. No-op on Windows + // (source equals destination) and on any run after the first. + migrate::migrateFilesByExtension(p.confDir, p.keymap.writable, ".map", "keymaps"); + migrate::migrateFilesByExtension(p.confDir, p.gamepad.writable, ".pad", "gamepads"); + migrate::migrateSingleFile(p.confDir / "boot.$B", + p.boot.writable / "boot.$B", "boot"); + migrate::migrateSingleFile(p.confDir / "debuga.layout", + p.cacheDir / "debuga.layout", "debuga.layout"); +} + +} // namespace + void conf_init(char* wpath, char* confdir) { - conf.scrShot.dir = std::string(getenv(ENVHOME)); + // Default screenshot output to the user's Pictures directory per the + // xdg-user-dirs spec. On Windows (where xdgpp's BaseDirectories throws + // because $HOME is unset) or whenever anything in that chain fails, fall + // back to $ENVHOME — which is HOMEPATH on Windows, HOME elsewhere. Users + // can override via config.conf's `scrDir` — that wins on subsequent loads. + try { + conf.scrShot.dir = xdg::PicturesDir().string(); + } catch (const std::exception &) { + conf.scrShot.dir = std::string(getenv(ENVHOME)); + } conf.port = 30000; + + PlatformRoots roots; #if defined(__linux) || defined(__APPLE__) || defined(__BSD) + const fs::path dataDirsSuffix = "samstyle/xpeccy"; + const char *home = getenv(ENVHOME); + + // Wrap an xdgpp single-path getter: append dataDirsSuffix on success, fall + // back to a precomposed path on exception. + auto xdgPathOrFallback = [&dataDirsSuffix](auto &&getter, + const fs::path &fallback, + std::string_view label) -> fs::path { + try { return getter() / dataDirsSuffix; } + catch (const std::exception &e) { + std::cout << label << ": " << e.what() << std::endl; + return fallback; + } + }; + // Wrap an xdgpp dir-list getter: each element gets dataDirsSuffix appended; + // returns an empty vector on exception. + auto xdgDirsOrEmpty = [&dataDirsSuffix](auto &&getter, + std::string_view label) -> std::vector { + std::vector out; + try { + for (auto &d : getter()) out.push_back(d / dataDirsSuffix); + } catch (const std::exception &e) { + std::cout << label << ": " << e.what() << std::endl; + } + return out; + }; + if (confdir == NULL) { - conf.path.confDir = std::string(getenv(ENVHOME)) + "/.config"; - mkdir(conf.path.confDir.c_str(), 0777); - conf.path.confDir += "/samstyle"; - mkdir(conf.path.confDir.c_str(), 0777); - conf.path.confDir += "/xpeccy"; - } else { - conf.path.confDir = std::string(confdir); - } - mkdir(conf.path.confDir.c_str(), 0777); - conf.path.romDir = conf.path.confDir + "/roms"; - mkdir(conf.path.romDir.c_str() ,0777); - conf.path.prfDir = conf.path.confDir + "/profiles"; - mkdir(conf.path.prfDir.c_str() ,0777); - conf.path.shdDir = conf.path.confDir + "/shaders"; - mkdir(conf.path.shdDir.c_str() ,0777); - conf.path.palDir = conf.path.confDir + "/palettes"; - mkdir(conf.path.palDir.c_str() ,0777); - conf.path.plgDir = conf.path.confDir + "/plugins"; - mkdir(conf.path.plgDir.c_str() ,0777); - conf.path.qssDir = conf.path.confDir + "/styles"; - mkdir(conf.path.qssDir.c_str() ,0777); - conf.path.confFile = conf.path.confDir + "/config.conf"; - conf.path.boot = conf.path.confDir + "/boot.$B"; -#elif defined(__WIN32) - if (confdir == NULL) { - conf.path.confDir = std::string(wpath); - size_t pos = conf.path.confDir.find_last_of(SLSH); - if (pos != std::string::npos) { - conf.path.confDir = conf.path.confDir.substr(0, pos); + roots.confHome = xdgPathOrFallback(xdg::ConfigHomeDir, + (home ? fs::path(home) : fs::path(".")) / ".config" / dataDirsSuffix, + "xdg config home"); + // Pre-XDG-aware migration: if XDG_CONFIG_HOME points somewhere other + // than ~/.config, move the old ~/.config/samstyle/xpeccy across. Must + // run before applyPlatformRoots creates the new confDir — otherwise + // migrateDir sees the destination already present and short-circuits. + if (home) { + migrate::migrateDir(fs::path(home) / ".config" / dataDirsSuffix, + roots.confHome, "confDir"); } - conf.path.confDir += "\\config"; } else { - conf.path.confDir = std::string(confdir); + roots.confHome = confdir; } - conf.path.romDir = conf.path.confDir + "\\roms"; - conf.path.prfDir = conf.path.confDir + "\\profiles"; - conf.path.shdDir = conf.path.confDir + "\\shaders"; - conf.path.palDir = conf.path.confDir + "\\palettes"; - conf.path.plgDir = conf.path.confDir + "\\plugins"; - conf.path.qssDir = conf.path.confDir + "\\styles"; - conf.path.confFile = conf.path.confDir + "\\config.conf"; - conf.path.boot = conf.path.confDir + "\\boot.$B"; - mkdir(conf.path.confDir.c_str()); - mkdir(conf.path.romDir.c_str()); - mkdir(conf.path.prfDir.c_str()); - mkdir(conf.path.shdDir.c_str()); - mkdir(conf.path.palDir.c_str()); - mkdir(conf.path.plgDir.c_str()); - mkdir(conf.path.qssDir.c_str()); + roots.dataHome = xdgPathOrFallback(xdg::DataHomeDir, roots.confHome, "xdg data home"); + roots.cacheHome = xdgPathOrFallback(xdg::CacheHomeDir, roots.confHome / "cache", "xdg cache home"); + roots.stateHome = xdgPathOrFallback(xdg::StateHomeDir, roots.confHome / "state", "xdg state home"); + roots.configDirs = xdgDirsOrEmpty(xdg::ConfigDirs, "xdg config dirs"); + roots.dataDirs = xdgDirsOrEmpty(xdg::DataDirs, "xdg data dirs"); +#elif defined(__WIN32) + roots.confHome = (confdir == NULL) + ? fs::path(wpath).parent_path() / "config" + : fs::path(confdir); + // No XDG equivalents on Windows; everything roots at confHome. The + // readonly dir vectors stay default-constructed (empty), making every + // resource's `readonly` list empty and the one-shot migrations no-ops + // (source paths equal destinations). + roots.dataHome = roots.confHome; + roots.cacheHome = roots.confHome / "cache"; + roots.stateHome = roots.confHome / "state"; #endif + applyPlatformRoots(roots); conf.scrShot.format = "png"; // Pentagon geometry: // rows: 16Vblk + (16 invis + 48 vis) top border + 192 screen + 48 bottom border = 320 rows @@ -114,168 +247,181 @@ void conf_init(char* wpath, char* confdir) { } void saveConfig() { - FILE* cfile = fopen(conf.path.confFile.c_str(), "wb"); - if (!cfile) { + std::ofstream out(conf.path.confFile, std::ios::binary); + if (!out) { shitHappens("Can't write main config"); throw(0); } + out << std::fixed << std::setprecision(6); // match old "%f" formatting + + out << "[GENERAL]\n\n"; + writeKV(out, "startdefault", YESNO(conf.defProfile)); + writeKV(out, "savepaths", YESNO(conf.storePaths)); + writeKV(out, "fdcturbo", YESNO(fdcFlag & FDC_FAST)); + writeKV(out, "addboot", YESNO(conf.boot)); + writeKV(out, "exit.confirm", YESNO(conf.confexit)); + writeKV(out, "port", conf.port); + writeKV(out, "winpos", conf.xpos, ',', conf.ypos); + writeKV(out, "flpinterleave", flp_get_interleave()); + writeKV(out, "style", conf.style); - fprintf(cfile,"[GENERAL]\n\n"); - fprintf(cfile, "startdefault = %s\n", YESNO(conf.defProfile)); - fprintf(cfile, "savepaths = %s\n", YESNO(conf.storePaths)); - fprintf(cfile, "fdcturbo = %s\n", YESNO(fdcFlag & FDC_FAST)); - fprintf(cfile, "addboot = %s\n", YESNO(conf.boot)); - fprintf(cfile, "exit.confirm = %s\n",YESNO(conf.confexit)); - fprintf(cfile, "port = %i\n", conf.port); - fprintf(cfile, "winpos = %i,%i\n",conf.xpos,conf.ypos); - fprintf(cfile, "flpinterleave = %i\n", flp_get_interleave()); - fprintf(cfile, "style = %s\n", conf.style.c_str()); - - fprintf(cfile, "\n[BOOKMARKS]\n\n"); + out << "\n[BOOKMARKS]\n\n"; foreach(xBookmark bkm, conf.bookmarkList) { - fprintf(cfile, "%s = %s\n", bkm.name.c_str(), bkm.path.c_str()); + writeKV(out, bkm.name, bkm.path); } - fprintf(cfile, "\n[PROFILES]\n\n"); - foreach(xProfile* prf, conf.prof.list) { // nr.0 skipped ('default' profile) + out << "\n[PROFILES]\n\n"; + foreach(xProfile* prf, conf.prof.list) { if (prf->name != "default") - fprintf(cfile, "%s = %s\n", prf->name.c_str(), prf->file.c_str()); + writeKV(out, prf->name, prf->file); } - fprintf(cfile, "current = %s\n", conf.prof.cur->name.c_str()); + writeKV(out, "current", conf.prof.cur->name); - fprintf(cfile, "\n[VIDEO]\n\n"); + out << "\n[VIDEO]\n\n"; foreach(xLayout lay, conf.layList) { if (lay.name != "default") { - fprintf(cfile, "layout = %s:%i:%i:%i:%i:%i:%i:%i:%i:%i:%i:%i\n",lay.name.c_str(),\ - lay.lay.full.x, lay.lay.full.y, lay.lay.bord.x, lay.lay.bord.y,\ - lay.lay.blank.x, lay.lay.blank.y, lay.lay.intSize, lay.lay.intpos.y, lay.lay.intpos.x,\ - lay.lay.scr.x, lay.lay.scr.y); + writeKV(out, "layout", + lay.name, + ':', lay.lay.full.x, ':', lay.lay.full.y, + ':', lay.lay.bord.x, ':', lay.lay.bord.y, + ':', lay.lay.blank.x, ':', lay.lay.blank.y, + ':', lay.lay.intSize, + ':', lay.lay.intpos.y, ':', lay.lay.intpos.x, + ':', lay.lay.scr.x, ':', lay.lay.scr.y); } } - fprintf(cfile, "scrDir = %s\n", conf.scrShot.dir.c_str()); - fprintf(cfile, "scrFormat = %s\n", conf.scrShot.format.c_str()); - fprintf(cfile, "scrCount = %i\n", conf.scrShot.count); - fprintf(cfile, "scrInterval = %i\n", conf.scrShot.interval); - fprintf(cfile, "scrNoLeds = %s\n", YESNO(conf.scrShot.noLeds)); - fprintf(cfile, "scrNoBord = %s\n", YESNO(conf.scrShot.noBorder)); - fprintf(cfile, "fullscreen = %s\n", YESNO(conf.vid.fullScreen)); - fprintf(cfile, "keepratio = %s\n", YESNO(conf.vid.keepRatio)); - fprintf(cfile, "scale = %i\n", conf.vid.scale); - fprintf(cfile, "greyscale = %s\n", YESNO(greyScale)); -// fprintf(cfile, "scanlines = %s\n", YESNO(scanlines)); - fprintf(cfile, "bordersize = %i\n", int(conf.brdsize * 100)); - fprintf(cfile, "noflick = %i\n", noflic); - fprintf(cfile, "noflick.mode = %i\n", noflicMode); - fprintf(cfile, "noflick.gamma = %f\n", noflicGamma); - fprintf(cfile, "shader = %s\n", conf.vid.shader.c_str()); - - fprintf(cfile, "\n[ROMSETS]\n"); + writeKV(out, "scrDir", conf.scrShot.dir); + writeKV(out, "scrFormat", conf.scrShot.format); + writeKV(out, "scrCount", conf.scrShot.count); + writeKV(out, "scrInterval", conf.scrShot.interval); + writeKV(out, "scrNoLeds", YESNO(conf.scrShot.noLeds)); + writeKV(out, "scrNoBord", YESNO(conf.scrShot.noBorder)); + writeKV(out, "fullscreen", YESNO(conf.vid.fullScreen)); + writeKV(out, "keepratio", YESNO(conf.vid.keepRatio)); + writeKV(out, "scale", conf.vid.scale); + writeKV(out, "greyscale", YESNO(greyScale)); + writeKV(out, "bordersize", int(conf.brdsize * 100)); + writeKV(out, "noflick", noflic); + writeKV(out, "noflick.mode", noflicMode); + writeKV(out, "noflick.gamma", noflicGamma); + writeKV(out, "shader", conf.vid.shader); + + out << "\n[ROMSETS]\n"; foreach(xRomset rms, conf.rsList) { - fprintf(cfile, "\nname = %s\n", rms.name.c_str()); + out << "\n"; + writeKV(out, "name", rms.name); foreach(xRomFile rf, rms.roms) { - fprintf(cfile, "rom = %s:%i:%i:%i\n",rf.name.c_str(), rf.foffset, rf.fsize, rf.roffset); + writeKV(out, "rom", + rf.name, + ':', rf.foffset, + ':', rf.fsize, + ':', rf.roffset); } - if (!rms.gsFile.empty()) - fprintf(cfile, "gs = %s\n", rms.gsFile.c_str()); - if (!rms.fntFile.empty()) - fprintf(cfile, "font = %s\n", rms.fntFile.c_str()); - if (!rms.vBiosFile.empty()) - fprintf(cfile, "vga = %s\n", rms.vBiosFile.c_str()); + if (!rms.gsFile.empty()) writeKV(out, "gs", rms.gsFile); + if (!rms.fntFile.empty()) writeKV(out, "font", rms.fntFile); + if (!rms.vBiosFile.empty()) writeKV(out, "vga", rms.vBiosFile); } - fprintf(cfile, "\n[SOUND]\n\n"); - fprintf(cfile, "enabled = %s\n", YESNO(conf.snd.enabled)); - fprintf(cfile, "soundsys = %s\n", sndOutput->name); - fprintf(cfile, "rate = %i\n", conf.snd.rate); - fprintf(cfile, "volume.master = %i\n", conf.snd.vol.master); - fprintf(cfile, "volume.beep = %i\n", conf.snd.vol.beep); - fprintf(cfile, "volume.tape = %i\n", conf.snd.vol.tape); - fprintf(cfile, "volume.ay = %i\n", conf.snd.vol.ay); - fprintf(cfile, "volume.gs = %i\n", conf.snd.vol.gs); - fprintf(cfile, "volume.sdrv = %i\n", conf.snd.vol.sdrv); - fprintf(cfile, "volume.saa = %i\n", conf.snd.vol.saa); - - fprintf(cfile, "\n[TAPE]\n\n"); - fprintf(cfile, "autoplay = %s\n", YESNO(conf.tape.autostart)); - fprintf(cfile, "fast = %s\n", YESNO(conf.tape.fast)); - - fprintf(cfile, "\n[INPUT]\n\n"); - fprintf(cfile, "gamepad = %s\n", conf.gpctrl->gpada->lastName().toLocal8Bit().data()); - fprintf(cfile, "deadzone = %i\n", conf.gpctrl->gpada->deadZone()); - fprintf(cfile, "gamepad2 = %s\n", conf.gpctrl->gpadb->lastName().toLocal8Bit().data()); - fprintf(cfile, "deadzone2 = %i\n", conf.gpctrl->gpadb->deadZone()); - - fprintf(cfile, "\n[LEDS]\n\n"); - fprintf(cfile, "mouse = %s\n", YESNO(conf.led.mouse)); - fprintf(cfile, "joystick = %s\n", YESNO(conf.led.joy)); - fprintf(cfile, "keyscan = %s\n", YESNO(conf.led.keys)); - fprintf(cfile, "tape = %s\n", YESNO(conf.led.tape)); - fprintf(cfile, "disk = %s\n", YESNO(conf.led.disk)); - fprintf(cfile, "message = %s\n", YESNO(conf.led.message)); - fprintf(cfile, "fps = %s\n", YESNO(conf.led.fps)); - fprintf(cfile, "halt = %s\n", YESNO(conf.led.halt)); - - fprintf(cfile, "\n[DEBUGA]\n\n"); - fprintf(cfile, "dbsize = %i\n", conf.dbg.dbsize); - fprintf(cfile, "dwsize = %i\n", conf.dbg.dwsize); - fprintf(cfile, "dmsize = %i\n", conf.dbg.dmsize); - fprintf(cfile, "scr.zoom = %i\n", conf.dbg.scrzoom); - fprintf(cfile, "font = %s\n", conf.dbg.font.toString().toUtf8().data()); - fprintf(cfile, "window = %i:%i:%i:%i\n",conf.dbg.pos.x(),conf.dbg.pos.y(),conf.dbg.siz.width(),conf.dbg.siz.height()); - - fprintf(cfile, "\n[PALETTE]\n\n"); - QStringList lst = conf.pal.keys(); - QString nam; - QColor col; - foreach(nam, lst) { - col = conf.pal[nam]; + out << "\n[SOUND]\n\n"; + writeKV(out, "enabled", YESNO(conf.snd.enabled)); + writeKV(out, "soundsys", sndOutput->name); + writeKV(out, "rate", conf.snd.rate); + writeKV(out, "volume.master", conf.snd.vol.master); + writeKV(out, "volume.beep", conf.snd.vol.beep); + writeKV(out, "volume.tape", conf.snd.vol.tape); + writeKV(out, "volume.ay", conf.snd.vol.ay); + writeKV(out, "volume.gs", conf.snd.vol.gs); + writeKV(out, "volume.sdrv", conf.snd.vol.sdrv); + writeKV(out, "volume.saa", conf.snd.vol.saa); + + out << "\n[TAPE]\n\n"; + writeKV(out, "autoplay", YESNO(conf.tape.autostart)); + writeKV(out, "fast", YESNO(conf.tape.fast)); + + out << "\n[INPUT]\n\n"; + writeKV(out, "gamepad", conf.gpctrl->gpada->lastName()); + writeKV(out, "deadzone", conf.gpctrl->gpada->deadZone()); + writeKV(out, "gamepad2", conf.gpctrl->gpadb->lastName()); + writeKV(out, "deadzone2", conf.gpctrl->gpadb->deadZone()); + + out << "\n[LEDS]\n\n"; + writeKV(out, "mouse", YESNO(conf.led.mouse)); + writeKV(out, "joystick", YESNO(conf.led.joy)); + writeKV(out, "keyscan", YESNO(conf.led.keys)); + writeKV(out, "tape", YESNO(conf.led.tape)); + writeKV(out, "disk", YESNO(conf.led.disk)); + writeKV(out, "message", YESNO(conf.led.message)); + writeKV(out, "fps", YESNO(conf.led.fps)); + writeKV(out, "halt", YESNO(conf.led.halt)); + + out << "\n[DEBUGA]\n\n"; + writeKV(out, "dbsize", conf.dbg.dbsize); + writeKV(out, "dwsize", conf.dbg.dwsize); + writeKV(out, "dmsize", conf.dbg.dmsize); + writeKV(out, "scr.zoom", conf.dbg.scrzoom); + writeKV(out, "font", conf.dbg.font.toString()); + writeKV(out, "window", + conf.dbg.pos.x(), ':', conf.dbg.pos.y(), + ':', conf.dbg.siz.width(), ':', conf.dbg.siz.height()); + + out << "\n[PALETTE]\n\n"; + foreach(QString nam, conf.pal.keys()) { + const QColor col = conf.pal[nam]; if (col.isValid()) - fprintf(cfile, "%s = %s\n", nam.toLocal8Bit().data(), col.name().toLocal8Bit().data()); + writeKV(out, nam.toUtf8().constData(), col.name()); } - fprintf(cfile, "\n[KEYS]\n\n"); + out << "\n[KEYS]\n\n"; xShortcut* tab = shortcut_tab(); - int i = 0; - while (tab[i].id > 0) { - fprintf(cfile, "%s = %s\n", tab[i].name, tab[i].seq.toString().toLocal8Bit().data()); - i++; + for (int i = 0; tab[i].id > 0; i++) { + writeKV(out, tab[i].name, tab[i].seq.toString()); } - fclose(cfile); } -void copyFile(const char* src, const char* dst) { - QFile fle(QString::fromLocal8Bit(src)); - fle.open(QFile::ReadOnly); - QByteArray fdata = fle.readAll(); - fle.close(); - fle.setFileName(QString::fromLocal8Bit(dst)); - if (fle.open(QFile::WriteOnly)) { - fle.write(fdata); - fle.close(); +void copyFile(const fs::path &src, const fs::path &dst) { + std::error_code ec; + fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); + if (ec) { + std::cout << "copyFile: " << src << " -> " << dst + << " failed: " << ec.message() << std::endl; } } +void copyResource(std::string_view src, const fs::path &dst) { + QFile in(QString::fromUtf8(src.data(), static_cast(src.size()))); + if (!in.open(QIODevice::ReadOnly)) { + std::cout << "copyResource: can't read " << src << std::endl; + return; + } + const QByteArray data = in.readAll(); + in.close(); + std::ofstream out(dst, std::ios::binary); + if (!out) { + std::cout << "copyResource: can't write " << dst << std::endl; + return; + } + out.write(data.constData(), data.size()); +} + // emulator config void loadConfig() { std::string soutnam = "NULL"; - //printf("%s\n",conf.path.confFile); std::ifstream file(conf.path.confFile); - char fname[FILENAME_MAX]; if (!file.good()) { printf("Main config is missing. Default files will be copied\n"); - copyFile(":/conf/config.conf", conf.path.confFile.c_str()); - strcpy(fname, conf.path.confDir.c_str()); - strcat(fname, SLASH); - strcat(fname, "xpeccy.conf"); - copyFile(":/conf/xpeccy.conf", fname); - strcpy(fname, conf.path.romDir.c_str()); - strcat(fname, SLASH); - strcat(fname, "1982.rom"); - copyFile(":/conf/1982.rom", fname); + copyResource(":/conf/config.conf", conf.path.confFile); + copyResource(":/conf/xpeccy.conf", conf.path.confDir / "xpeccy.conf"); + // Only seed the default ROM if no system-wide copy already resolves — + // otherwise we'd permanently shadow a distro-shipped 1982.rom with a + // private copy that never gets updated. + if (!conf.path.rom.tryFind("1982.rom")) { + copyResource(":/conf/1982.rom", + conf.path.rom.writable / "1982.rom"); + } file.open(conf.path.confFile); if (!file.good()) { - printf("%s\n",conf.path.confFile.c_str()); + std::cout << conf.path.confFile << std::endl; shitHappens("Doh! Something going wrong"); throw(0); } diff --git a/src/xcore/gamepad.cpp b/src/xcore/gamepad.cpp index aaf79d38..bff9701c 100644 --- a/src/xcore/gamepad.cpp +++ b/src/xcore/gamepad.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "xcore.h" @@ -113,22 +114,20 @@ void xGamepad::delItem(int i) { void xGamepad::loadMap(std::string mapname) { if (mapname.empty()) return; + const auto maybe = conf.path.gamepad.tryFind(mapname); + if (!maybe) return; + std::ifstream file(*maybe); + if (!file) return; xJoyMapEntry jent; - FILE* file; int num; int idx; char buf[1024]; char* ptr; - std::string path = conf.path.confDir + SLASH + mapname; - file = fopen(path.c_str(), "rb"); - if (file) { - map.clear(); - while(!feof(file)) { - memset(buf, 0x00, 1024); - fgets(buf, 1023, file); - ptr = strtok(buf, ":\n"); - if (ptr) { + map.clear(); + while (file.getline(buf, sizeof(buf) - 1)) { + ptr = strtok(buf, ":\n"); + if (ptr) { jent.type = padGetId(ptr[0], pabhChars); jent.num = atoi(&ptr[1]); idx = 1; @@ -193,75 +192,60 @@ void xGamepad::loadMap(std::string mapname) { if (jent.dev != JMAP_NONE) map.push_back(jent); } - } } - fclose(file); } } void xGamepad::saveMap(std::string mapname) { if (mapname.empty()) return; - std::string path = conf.path.confDir + SLASH + mapname; - FILE* file; - file = fopen(path.c_str(), "wb"); - if (file) { - foreach(xJoyMapEntry jent, map) { - fprintf(file, "%c%i", padGetChar(jent.type, pabhChars), jent.num); - switch (jent.type) { - case JOY_AXIS: - fputc((jent.state < 0) ? '-' : '+', file); - break; - case JOY_HAT: - fputc(padGetChar(jent.state, hatChars), file); - break; - } - fprintf(file, ":%c", padGetChar(jent.dev, devChars)); - switch(jent.dev) { - case JMAP_KEY: + std::ofstream out(conf.path.gamepad.writable / mapname, std::ios::binary); + if (!out) return; + foreach(xJoyMapEntry jent, map) { + out << padGetChar(jent.type, pabhChars) << jent.num; + switch (jent.type) { + case JOY_AXIS: out << ((jent.state < 0) ? '-' : '+'); break; + case JOY_HAT: out << padGetChar(jent.state, hatChars); break; + } + out << ':' << padGetChar(jent.dev, devChars); + switch(jent.dev) { + case JMAP_KEY: #if USE_SEQ_BIND - fprintf(file, "%s", jent.seq.toString().toUtf8().data()); + out << jent.seq.toString(); #else - fprintf(file, "%s", getKeyNameById(jent.key)); + out << getKeyNameById(jent.key); #endif - break; - case JMAP_JOY: - case JMAP_JOYB: - fputc(padGetChar(jent.dir, kjoyChars), file); - break; - case JMAP_MOUSE: - fputc(padGetChar(jent.dir, kmouChars), file); - break; - default: - fprintf(file, "?"); - } - if (jent.rpt > 0) - fprintf(file, ":%i", jent.rpt); - fputc('\n', file); + break; + case JMAP_JOY: + case JMAP_JOYB: + out << padGetChar(jent.dir, kjoyChars); + break; + case JMAP_MOUSE: + out << padGetChar(jent.dir, kmouChars); + break; + default: + out << '?'; } - fclose(file); + if (jent.rpt > 0) out << ':' << jent.rpt; + out << '\n'; } } int padExists(std::string name) { - std::string path = conf.path.confDir + SLASH + name; - FILE* file = fopen(path.c_str(), "rb"); - if (!file) return 0; - fclose(file); - return 1; + return conf.path.gamepad.tryFind(name).has_value(); } int padCreate(std::string name) { if (padExists(name)) return 0; - std::string path = conf.path.confDir + SLASH + name; - FILE* file = fopen(path.c_str(), "wb"); - if (!file) return 0; - fclose(file); - return 1; + return std::ofstream(conf.path.gamepad.writable / name, + std::ios::binary).is_open(); } +// Only removes a user-writable copy. A same-named read-only pad map shipped +// under /usr/share/.../gamepads/ remains intact — deleting the writable copy +// just lets the system default shine through again. void padDelete(std::string name) { - std::string path = conf.path.confDir + SLASH + name; - remove(path.c_str()); + std::error_code ec; + fs::remove(conf.path.gamepad.writable / name, ec); } // xGamepad diff --git a/src/xcore/keymap.cpp b/src/xcore/keymap.cpp index 008d155f..be155f7f 100644 --- a/src/xcore/keymap.cpp +++ b/src/xcore/keymap.cpp @@ -202,38 +202,32 @@ void initKeyMap() { void loadKeys() { xProfile* prf = conf.prof.cur; if (!prf) return; - std::string sfnam = conf.path.confDir + SLASH + prf->kmapName; initKeyMap(); if ((prf->kmapName == "") || (prf->kmapName == "default")) return; - std::ifstream file(sfnam); + const auto maybe = conf.path.keymap.tryFind(prf->kmapName); + if (!maybe) { + printf("Can't find keymap '%s'. Default one will be used\n", + prf->kmapName.c_str()); + return; + } + std::ifstream file(*maybe); if (!file.good()) { - sfnam = conf.path.confDir + SLASH + "keymaps" + SLASH + prf->kmapName; - file.open(sfnam); - if (!file.good()) { - printf("Can't open keyboard layout. Default one will be used\n"); - return; - } + printf("Can't open keyboard layout. Default one will be used\n"); + return; } - char buf[1024]; -// std::pair spl; std::string line; - std::vector vec; - char keys[8]; - int rlen; - unsigned int i; - while (!file.eof()) { - file.getline(buf,1023); - line = std::string(buf); - vec = splitstr(line,"\t"); - memset(keys, 0, 8); - rlen = 0; + while (std::getline(file, line)) { + const auto vec = splitstr(line, "\t"); if (vec.size() > 0) { - for(i = 1; (rlen < KEYSEQ_MAXLEN) && (i < vec.size()); i++) { + std::string keys; + size_t rlen = 0; + for (size_t i = 1; i < vec.size() && rlen < KEYSEQ_MAXLEN; i++) { rlen += vec[i].size(); - if (rlen < KEYSEQ_MAXLEN) - strcat(keys, vec[i].c_str()); + if (rlen < KEYSEQ_MAXLEN) { + keys += vec[i]; + } } - setKey(vec[0].c_str(), keys); + setKey(vec[0].c_str(), keys.c_str()); } } } diff --git a/src/xcore/migrate.cpp b/src/xcore/migrate.cpp new file mode 100644 index 00000000..d74f3658 --- /dev/null +++ b/src/xcore/migrate.cpp @@ -0,0 +1,87 @@ +#include "migrate.h" + +#include +#include +#include +#include +#include + +namespace migrate { + +namespace { + +std::string toLowerCopy(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return std::tolower(c); }); + return s; +} + +// Whether a move targets a single regular file or a whole directory tree. +// Selects which copy/remove pair to use on the cross-device fallback path. +enum class MoveKind { SingleFile, DirectoryTree }; + +// Try fs::rename; if the target is on another filesystem (EXDEV), fall back +// to copy-then-remove with the kind-appropriate operations. Logs the attempt +// and any final error. The shared engine underneath the three public +// migrate* wrappers; nothing outside this TU needs it. +void moveAcrossDevices(const fs::path &from, const fs::path &to, MoveKind kind, + std::string_view what) { + std::cout << "Moving " << from << " -> " << to << std::endl; + std::error_code ec; + fs::rename(from, to, ec); + if (ec == std::errc::cross_device_link) { + ec.clear(); + if (kind == MoveKind::SingleFile) { + fs::copy_file(from, to, ec); + } else { + fs::copy(from, to, fs::copy_options::recursive, ec); + } + if (!ec) { + if (kind == MoveKind::SingleFile) fs::remove(from, ec); + else fs::remove_all(from, ec); + } + } + if (ec) { + std::cout << "legacy " << what << " migration failed: " + << ec.message() << std::endl; + } +} + +} // namespace + +void migrateSingleFile(const fs::path &from, const fs::path &to, + std::string_view what) { + std::error_code ec; + if (!fs::exists(from, ec) || fs::exists(to, ec)) return; + fs::create_directories(to.parent_path(), ec); + moveAcrossDevices(from, to, MoveKind::SingleFile, what); +} + +void migrateDir(const fs::path &from, const fs::path &to, + std::string_view what) { + std::error_code ec; + if (!fs::exists(from, ec) || fs::exists(to, ec)) return; + fs::create_directories(to.parent_path(), ec); + moveAcrossDevices(from, to, MoveKind::DirectoryTree, what); +} + +void migrateFilesByExtension(const fs::path &from, const fs::path &to, + std::string_view ext, std::string_view what) { + if (from == to) return; + std::error_code ec; + if (!fs::exists(from, ec) || !fs::is_directory(from, ec)) return; + fs::create_directories(to, ec); + const std::string wantExt = toLowerCopy(std::string{ext}); + for (auto it = fs::directory_iterator(from, ec); + !ec && it != fs::directory_iterator(); + it.increment(ec)) { + std::error_code fec; + if (!it->is_regular_file(fec)) continue; + if (toLowerCopy(it->path().extension().string()) != wantExt) continue; + const fs::path dst = to / it->path().filename(); + if (fs::exists(dst, fec)) continue; // don't clobber existing + moveAcrossDevices(it->path(), dst, MoveKind::SingleFile, what); + } +} + +} // namespace migrate diff --git a/src/xcore/migrate.h b/src/xcore/migrate.h new file mode 100644 index 00000000..fdc7fc4f --- /dev/null +++ b/src/xcore/migrate.h @@ -0,0 +1,39 @@ +#pragma once + +// One-shot filesystem migration primitives used by conf_init and profile +// loading to move pre-XDG layouts into the new per-kind dirs. Kept in one +// place so the rename-or-copy-on-EXDEV idiom lives in exactly one function +// and so every legacy-window comment sits next to the helpers it uses. The +// whole header can be deleted when the XDG transition window closes. + +#include +#include + +namespace fs = std::filesystem; + +// Public migration API — three "what to migrate" shapes: a single file, a +// whole directory, or every file with a given extension. All three share an +// internal rename-or-copy-on-EXDEV engine; callers treat these as +// fire-and-forget (error handling and logging are internal). +namespace migrate { + +// If `from` exists and `to` does not, move `from` → `to`. No-op otherwise. +// The "legacy file hanging around; move it if present, ignore it if not" +// idiom. +void migrateSingleFile(const fs::path &from, const fs::path &to, + std::string_view what); + +// Move an existing directory to a new location. No-op if `from` doesn't +// exist or `to` already does. Falls back to recursive copy + remove on +// cross-device moves. +void migrateDir(const fs::path &from, const fs::path &to, + std::string_view what); + +// Move every regular file whose extension matches `ext` (case-insensitive, +// dot included, e.g. ".map") from `from` into `to`. Preserves filenames; +// skips files whose destination already exists. Intended for flattening +// pre-subdir resource files sitting at the root of confDir. +void migrateFilesByExtension(const fs::path &from, const fs::path &to, + std::string_view ext, std::string_view what); + +} // namespace migrate diff --git a/src/xcore/palette.cpp b/src/xcore/palette.cpp index 4a6ce536..11147c8a 100644 --- a/src/xcore/palette.cpp +++ b/src/xcore/palette.cpp @@ -5,57 +5,34 @@ // load preset colors for zx palette QList loadColors(std::string fname) { - QList list; - QColor col; - std::string path = conf.path.palDir + SLASH + fname; - QFile file(path.c_str()); - int i = 0; - QString line; - QString hexPart; - uint rgb; - bool ok; - int pos; - if (file.open(QFile::ReadOnly)) { - while (!file.atEnd() && (i < 16)) { - line = file.readLine(); - pos = line.indexOf('#'); - if ((pos >= 0) && ((pos + 6) < line.size())) { - hexPart = line.mid(pos + 1, 6); - rgb = hexPart.toUInt(&ok, 16); - if (ok) { - col.setRed((rgb >> 16) & 0xff); - col.setGreen((rgb >> 8) & 0xff); - col.setBlue(rgb & 0xff); - list.append(col); - i++; - } - } - } - file.close(); + QFile file(toQString(conf.path.palette.find(fname))); + if (!file.open(QFile::ReadOnly)) return {}; + QList colors; + while (!file.atEnd() && colors.size() < 16) { + const QString line = file.readLine(); + const int pos = line.indexOf('#'); + if (pos < 0 || pos + 6 >= line.size()) continue; + bool ok; + const uint rgb = line.mid(pos + 1, 6).toUInt(&ok, 16); + if (ok) colors.append(QColor::fromRgb((rgb >> 16) & 0xff, + (rgb >> 8) & 0xff, + rgb & 0xff)); } - return list; + return colors; } int saveColors(std::string fname, QList pal) { if (pal.size() < 16) return ERR_SIZE; - std::string path = conf.path.palDir + SLASH + fname; - QFile file(path.c_str()); - int err = ERR_OK; - QColor col; - QString str; - if (file.open(QFile::WriteOnly)) { - foreach(col, pal) { - str = "#"; - str.append(gethexbyte(col.red())); - str.append(gethexbyte(col.green())); - str.append(gethexbyte(col.blue())); - file.write(str.toLocal8Bit()); - file.write("\r\n"); - } - } else { - err = ERR_CANT_OPEN; + QFile file(toQString(conf.path.palette.writable / fname)); + if (!file.open(QFile::WriteOnly)) return ERR_CANT_OPEN; + for (const QColor &col : pal) { + file.write(QString("#%1%2%3\r\n") + .arg(gethexbyte(col.red()), + gethexbyte(col.green()), + gethexbyte(col.blue())) + .toLocal8Bit()); } - return err; + return ERR_OK; } void loadPalette(xProfile* prf) { diff --git a/src/xcore/profiles.cpp b/src/xcore/profiles.cpp index 93b5c541..6938894c 100644 --- a/src/xcore/profiles.cpp +++ b/src/xcore/profiles.cpp @@ -3,46 +3,88 @@ #include #include #include +#include +#include #include #include #include "xcore.h" +#include "migrate.h" #include "../xgui/xgui.h" -#include "sound.h" #include "filer.h" #include "gamepad.h" -void prf_load_cmos(xProfile* prf, std::string path) { - FILE* file = fopen(path.c_str(), "rb"); - if (file) { - fread((char*)prf->zx->cmos.data, 256, 1, file); - fclose(file); - } +namespace { + +// Open a file in binary mode and read up to `bytes` bytes into `dest`. +// Returns true iff the file was opened. A short read (truncated file) is +// *not* reported as failure — this matches the pre-existing fread-with- +// ignored-return behavior the old code had for ROM loading. +template +bool loadFixedBlob(const fs::path &path, T *dest, std::size_t bytes) { + std::ifstream f(path, std::ios::binary); + if (!f) return false; + f.read(reinterpret_cast(dest), bytes); + return true; } -void prf_save_cmos(xProfile* prf, std::string path) { - FILE* file = fopen(path.c_str(), "wb"); - if (file) { - fwrite((char*)prf->zx->cmos.data, 256, 1, file); - fclose(file); - } +// Open a file in binary mode and write exactly `bytes` bytes from `src`. +// Silently no-ops on open failure. +template +void saveFixedBlob(const fs::path &path, const T *src, std::size_t bytes) { + std::ofstream f(path, std::ios::binary); + if (f) f.write(reinterpret_cast(src), bytes); } -void prf_load_nvram(xProfile* prf, std::string path) { - FILE* file = fopen(path.c_str(), "rb"); - if (file) { - fread((char*)prf->zx->ide->smuc.nv->mem, 256, 1, file); - fclose(file); +// Canonical per-profile directories. Config dir holds the user-editable .conf; +// state dir holds machine-authored cmos/nvram blobs. Both are keyed by profile +// name. Single-source-of-truth so the "/" pattern doesn't drift. +fs::path profileConfigDir(const xProfile *prf) { + return conf.path.prfDir / prf->name; +} +fs::path profileStateDir(const xProfile *prf) { + return conf.path.prfStateDir / prf->name; +} + +// Canonical location for a per-profile state blob (cmos/nvram) under the XDG +// state-home tree. +fs::path profileStatePath(const xProfile *prf, std::string_view ext) { + return profileStateDir(prf) / (prf->name + std::string(ext)); +} + +// One-shot migration of a per-profile state file from older locations into the +// state-home tree. Checks in order: current (stateDir) → legacy profDir → deep +// legacy confDir root. First legacy hit wins; migrateSingleFile short-circuits +// any remaining attempts because `to` now exists. +void migrateProfileState(const xProfile *prf, std::string_view ext) { + const fs::path target = profileStatePath(prf, ext); + const std::string leaf = prf->name + std::string(ext); + const fs::path legacy[] = { + profileConfigDir(prf) / leaf, + conf.path.confDir / leaf, + }; + for (const auto &src : legacy) { + migrate::migrateSingleFile(src, target, "profile state"); } } -void prf_save_nvram(xProfile* prf, std::string path) { +} // namespace + +void prf_load_cmos(xProfile* prf, const fs::path &path) { + loadFixedBlob(path, prf->zx->cmos.data, 256); +} + +void prf_save_cmos(xProfile* prf, const fs::path &path) { + saveFixedBlob(path, prf->zx->cmos.data, 256); +} + +void prf_load_nvram(xProfile* prf, const fs::path &path) { + loadFixedBlob(path, prf->zx->ide->smuc.nv->mem, 256); +} + +void prf_save_nvram(xProfile* prf, const fs::path &path) { if (prf->zx->ide->type != IDE_SMUC) return; - FILE* file = fopen(path.c_str(), "wb"); - if (file) { - fwrite((char*)prf->zx->ide->smuc.nv->mem, 256, 1, file); - fclose(file); - } + saveFixedBlob(path, prf->zx->ide->smuc.nv->mem, 256); } xProfile* findProfile(std::string nm) { @@ -64,15 +106,12 @@ xProfile* addProfile(std::string nm, std::string fp) { nprof->layName = std::string("default"); nprof->zx = compCreate(); nprof->curlabset = nullptr; - std::string fname; - fname = conf.path.prfDir + SLASH + nprof->name; -#if defined(__linux) || defined(__APPLE__) || defined(__BSD) - mkdir(fname.c_str(), 0777); -#elif defined(__WIN32) - mkdir(fname.c_str()); -#endif - prf_load_cmos(nprof, conf.path.prfDir + SLASH + nprof->name + SLASH + nprof->name + ".cmos"); - prf_load_nvram(nprof, conf.path.prfDir + SLASH + nprof->name + SLASH + nprof->name + ".nvram"); + std::error_code ec; + fs::create_directories(profileConfigDir(nprof), ec); + migrateProfileState(nprof, ".cmos"); + migrateProfileState(nprof, ".nvram"); + prf_load_cmos(nprof, profileStatePath(nprof, ".cmos")); + prf_load_nvram(nprof, profileStatePath(nprof, ".nvram")); prfSetHardware(nprof,"Dummy"); conf.prof.list.push_back(nprof); return nprof; @@ -89,9 +128,9 @@ int copyProfile(std::string src, std::string dst) { } else { dprf->file = dfile; } - std::string sfname = conf.path.prfDir + SLASH + sprf->name + SLASH + sprf->file; - std::string dfname = conf.path.prfDir + SLASH + dprf->name + SLASH + dfile; - copyFile(sfname.c_str(), dfname.c_str()); + const fs::path sfname = profileConfigDir(sprf) / sprf->file; + const fs::path dfname = profileConfigDir(dprf) / dfile; + copyFile(sfname, dfname); prfLoad(dst); return 1; } @@ -107,8 +146,6 @@ int delProfile(std::string nm) { if (prf == NULL) return DELP_ERR; // no such profile if (prf->name == "default") return DELP_ERR; // can't touch this int res = DELP_OK; - std::string cpath; - std::string cdir; // set default profile if current deleted if (conf.prof.cur) { if (conf.prof.cur->name == nm) { @@ -121,15 +158,15 @@ int delProfile(std::string nm) { // remove all such profiles from list & free mem for (int i = 0; i < conf.prof.list.size(); i++) { if (conf.prof.list[i]->name == nm) { - cdir = conf.path.prfDir + SLASH + prf->name + SLASH; - cpath = cdir + prf->file; - remove(cpath.c_str()); // remove config file - cpath = cdir + prf->name + ".cmos"; - remove(cpath.c_str()); // remove cmos dump - cpath = cdir + prf->name + ".nvram"; - remove(cpath.c_str()); // remove nvram dump - rmdir(cdir.c_str()); // remove directory (leave it if there is files) - compDestroy(prf->zx); // delete computer + const fs::path confDir = profileConfigDir(prf); + const fs::path stateDir = profileStateDir(prf); + std::error_code ec; + fs::remove(confDir / prf->file, ec); + fs::remove(profileStatePath(prf, ".cmos"), ec); + fs::remove(profileStatePath(prf, ".nvram"), ec); + fs::remove(confDir, ec); // remove directories (leaves them if non-empty) + fs::remove(stateDir, ec); + compDestroy(prf->zx); // delete computer delete(prf); conf.prof.list.erase(conf.prof.list.begin() + i); } @@ -227,85 +264,84 @@ void prfSetRomset(xProfile* prf, std::string rnm) { prf = conf.prof.cur; prf->rsName = rnm; xRomset* rset = findRomset(rnm); - std::string fpath; + if (!rset) return; int romsz = MEM_256; // prf->zx->mem->romSize; // 0? - int foff; - int fsze; - int roff; - FILE* file; - if (rset) { - memset(prf->zx->mem->romData, 0xff, MEM_512K); - foreach(xRomFile xrf, rset->roms) { - foff = xrf.foffset * 1024; - roff = xrf.roffset * 1024; - fpath = conf.path.romDir + SLASH + xrf.name; - file = fopen(fpath.c_str(), "rb"); - if (file) { - if (xrf.fsize <= 0) { // check part size - fseek(file, 0, SEEK_END); - fsze = ftell(file); - rewind(file); - } else { - fsze = xrf.fsize * 1024; - } - if (roff + fsze > romsz) { // check crossing rom top - romsz = toLimits(roff + fsze, MEM_256, MEM_512K); - romsz = toPower(romsz); - } - if (roff + fsze > romsz) // check again (if 512K limit) - fsze = romsz - roff; - if ((foff >= 0) && (roff >= 0) && (roff < MEM_512K) && (fsze > 0)) { // load rom if all is ok - fseek(file, foff, SEEK_SET); - fread(prf->zx->mem->romData + roff, fsze, 1, file); - } - fclose(file); - } else { - printf("Can't load rom file '%s'\n",fpath.c_str()); - } + + memset(prf->zx->mem->romData, 0xff, MEM_512K); + foreach(xRomFile xrf, rset->roms) { + const int foff = xrf.foffset * 1024; + const int roff = xrf.roffset * 1024; + const auto maybePath = conf.path.rom.tryFind(xrf.name); + if (!maybePath) { + printf("Can't find rom '%s' in any search path\n", xrf.name.c_str()); + continue; + } + std::ifstream file(*maybePath, std::ios::binary); + if (!file) { + std::cout << "Can't load rom file " << *maybePath << std::endl; + continue; + } + std::error_code ec; + int fsze = (xrf.fsize <= 0) + ? static_cast(fs::file_size(*maybePath, ec)) + : xrf.fsize * 1024; + if (roff + fsze > romsz) { // check crossing rom top + romsz = toLimits(roff + fsze, MEM_256, MEM_512K); + romsz = toPower(romsz); } - memSetSize(prf->zx->mem, -1, romsz); + if (roff + fsze > romsz) // check again (if 512K limit) + fsze = romsz - roff; + if ((foff >= 0) && (roff >= 0) && (roff < MEM_512K) && (fsze > 0)) { // load rom if all is ok + file.seekg(foff); + file.read(reinterpret_cast(prf->zx->mem->romData + roff), fsze); + } + } + memSetSize(prf->zx->mem, -1, romsz); // load GS ROM - if (!rset->gsFile.empty()) { - fpath = conf.path.romDir + SLASH + rset->gsFile; - file = fopen(fpath.c_str(), "rb"); - if (file) { - fread(prf->zx->gs->mem->romData, MEM_32K, 1, file); - fclose(file); - } else { - printf("Can't load gs rom '%s' (profile %s)\n", fpath.c_str(), prf->name.c_str()); + if (!rset->gsFile.empty()) { + if (const auto path = conf.path.rom.tryFind(rset->gsFile)) { + if (!loadFixedBlob(*path, prf->zx->gs->mem->romData, MEM_32K)) { + std::cout << "Can't load gs rom " << *path + << " (profile " << prf->name << ")" << std::endl; memset((char*)prf->zx->gs->mem->romData, 0xff, MEM_32K); } - } -// load font data - if (!rset->fntFile.empty()) { - fpath = conf.path.romDir + SLASH + rset->fntFile; - vid_fnt_load(prf->zx->vid, fpath.c_str()); -/* - file = fopen(fpath.c_str(), "rb"); - if (file) { - fread(prf->zx->vid->font, MEM_8K, 1, file); - fclose(file); - } -*/ } else { - vid_fnt_del(prf->zx->vid); + printf("Can't find gs rom '%s' in any search path (profile %s)\n", + rset->gsFile.c_str(), prf->name.c_str()); + memset((char*)prf->zx->gs->mem->romData, 0xff, MEM_32K); } + } +// load font data. vid_fnt_load is called unconditionally with a path (tryFind's +// value or the synthesized writable-dir fallback) to match the pre-refactor +// behaviour — the C API handles a nonexistent path gracefully and may also do +// per-call teardown that we don't want to skip. + if (!rset->fntFile.empty()) { + const auto maybePath = conf.path.rom.tryFind(rset->fntFile); + const fs::path path = maybePath.value_or( + conf.path.rom.writable / rset->fntFile); + vid_fnt_load(prf->zx->vid, path.string().c_str()); + if (!maybePath) { + printf("Can't find font '%s' in any search path\n", rset->fntFile.c_str()); + } + } else { + vid_fnt_del(prf->zx->vid); + } // load ega/vga bios (64K max) - memset(prf->zx->vid->bios, 0xff, MEM_64K); - prf->zx->vid->vga.cga = 1; - if (!rset->vBiosFile.empty()) { - fpath = conf.path.romDir + SLASH + rset->vBiosFile; - file = fopen(fpath.c_str(), "rb"); - if (file) { - fread(prf->zx->vid->bios, MEM_64K, 1, file); - fclose(file); + memset(prf->zx->vid->bios, 0xff, MEM_64K); + prf->zx->vid->vga.cga = 1; + if (!rset->vBiosFile.empty()) { + if (const auto path = conf.path.rom.tryFind(rset->vBiosFile)) { + if (loadFixedBlob(*path, prf->zx->vid->bios, MEM_64K)) { prf->zx->vid->vga.cga = 0; } + } else { + printf("Can't find VGA bios '%s' in any search path\n", + rset->vBiosFile.c_str()); } } } -int prf_load_conf(xProfile* prf, std::string cfname, int flag) { +int prf_load_conf(xProfile* prf, const fs::path &cfname, int flag) { Computer* comp = prf->zx; Floppy* flp; int i; @@ -324,7 +360,7 @@ int prf_load_conf(xProfile* prf, std::string cfname, int flag) { int section = PS_NONE; if (!file.good() && flag) { printf("Profile config is missing. Default one will be created\n"); - copyFile(":/conf/xpeccy.conf", cfname.c_str()); + copyResource(":/conf/xpeccy.conf", cfname); file.open(cfname, std::ifstream::in); } if (!file.good()) { @@ -426,8 +462,16 @@ int prf_load_conf(xProfile* prf, std::string cfname, int flag) { if (pspl.second.empty()) { // no @, use built-in cpu_set_type(comp->cpu, pval.c_str(), NULL, NULL); } else { - str = conf.path.plgDir + SLASH + "cpu"; - cpu_set_type(comp->cpu, pspl.first.c_str(), str.c_str(), pspl.second.c_str()); + const auto maybePath = conf.path.pluginCpu.tryFind(pspl.second); + if (maybePath) { + const std::string dir = maybePath->parent_path().string(); + cpu_set_type(comp->cpu, pspl.first.c_str(), + dir.c_str(), pspl.second.c_str()); + } else { + printf("Can't find cpu plugin '%s' in any search path\n", + pspl.second.c_str()); + cpu_set_type(comp->cpu, pspl.first.c_str(), NULL, NULL); + } } } if (pnam == "cpu.frq") { @@ -509,8 +553,8 @@ int prf_load_conf(xProfile* prf, std::string cfname, int flag) { tmp2 = PLOAD_OK; if (!prfSetHardware(prf, prf->hwName)) { - sprintf(buf, "Profile: %s\nHardware was set to 'dummy'", prf->name.c_str()); - shitHappens(buf); + const std::string msg = "Profile: " + prf->name + "\nHardware was set to 'dummy'"; + shitHappens(msg.c_str()); tmp2 = PLOAD_HW; prfSetHardware(prf, "Dummy"); } else if (conf.storePaths) { // loading files @@ -550,30 +594,24 @@ int prf_load_conf(xProfile* prf, std::string cfname, int flag) { int prfLoad(std::string nm) { xProfile* prf = findProfile(nm); if (prf == NULL) return PLOAD_NF; - //char cfname[FILENAME_MAX]; - std::string cfname = conf.path.prfDir + SLASH + prf->name + SLASH + prf->file; // new location: $CONFDIR/profiles/$PROFILENAME/$FILENAME - std::string ofname = conf.path.confDir + SLASH + prf->file; // old location: $CONFDIR/$FILENAME - - std::string ocmos = conf.path.confDir + SLASH + prf->name + ".cmos"; - std::string ncmos = conf.path.prfDir + SLASH + prf->name + SLASH + prf->name + ".cmos"; - std::string onvr = conf.path.confDir + SLASH + prf->name + ".nvram"; - std::string nnvr = conf.path.prfDir + SLASH + prf->name + SLASH + prf->name + ".nvram"; + const fs::path cfname = profileConfigDir(prf) / prf->file; // current: $CONFDIR/profiles/$NAME/$FILE + const fs::path ofname = conf.path.confDir / prf->file; // deep legacy: $CONFDIR/$FILE + // Pre-subdir layout: the .conf, .cmos, and .nvram all lived flat in + // confDir. If ofname exists, migrate .conf alongside the usual state- + // file migration so the old flat layout gets fully cleared. + std::error_code ec; int res = prf_load_conf(prf, ofname, 0); if (res == PLOAD_OK) { - copyFile(ofname.c_str(), cfname.c_str()); // copy old file to new location - remove(ofname.c_str()); // remove old conf file - prf_load_cmos(prf, ocmos); - copyFile(ocmos.c_str(), ncmos.c_str()); - remove(ocmos.c_str()); - prf_load_nvram(prf, onvr); - copyFile(onvr.c_str(), nnvr.c_str()); - remove(onvr.c_str()); + copyFile(ofname, cfname); + fs::remove(ofname, ec); } else { res = prf_load_conf(prf, cfname, 1); - prf_load_cmos(prf, ncmos); - prf_load_nvram(prf, nnvr); } + migrateProfileState(prf, ".cmos"); + migrateProfileState(prf, ".nvram"); + prf_load_cmos(prf, profileStatePath(prf, ".cmos")); + prf_load_nvram(prf, profileStatePath(prf, ".nvram")); return res; } @@ -603,125 +641,117 @@ int prfSave(std::string nm) { if (prf == NULL) return PSAVE_NF; Computer* comp = prf->zx; - std::string cfname; - cfname = conf.path.prfDir + SLASH + prf->name; -#if defined(__linux) || defined(__APPLE__) || defined(__BSD) - mkdir(cfname.c_str(), 0777); -#elif defined(__WIN32) - mkdir(cfname.c_str()); -#endif - cfname = cfname + SLASH + prf->file; -// cfname = conf.path.confDir + SLASH + prf->file; // old file location + std::error_code ec; + fs::create_directories(profileConfigDir(prf), ec); + fs::create_directories(profileStateDir(prf), ec); + const fs::path cfname = profileConfigDir(prf) / prf->file; - prf_save_cmos(prf, conf.path.prfDir + SLASH + prf->name + SLASH + prf->name + ".cmos"); - prf_save_nvram(prf, conf.path.prfDir + SLASH + prf->name + SLASH + prf->name + ".nvram"); + prf_save_cmos(prf, profileStatePath(prf, ".cmos")); + prf_save_nvram(prf, profileStatePath(prf, ".nvram")); - FILE* file = fopen(cfname.c_str(), "wb"); - if (!file) { + std::ofstream out(cfname, std::ios::binary); + if (!out) { printf("Can't write settings\n"); return PSAVE_OF; } + out << std::fixed << std::setprecision(6); // match old "%f" formatting + + // Small adapter for the one C-string field that may be null; the ternary + // noise would otherwise pollute every line it appears in. + auto orEmpty = [](const char *s) -> const char * { return s ? s : ""; }; - fprintf(file, "[GENERAL]\n\n"); - fprintf(file, "lastdir = %s\n", prf->lastDir.c_str()); + out << "[GENERAL]\n\n"; + writeKV(out, "lastdir", prf->lastDir); - fprintf(file, "\n[MACHINE]\n\n"); - fprintf(file, "current = %s\n", prf->hwName.c_str()); - fprintf(file, "memory = %i\n", comp->mem->ramSize >> 10); // bytes to KB + out << "\n[MACHINE]\n\n"; + writeKV(out, "current", prf->hwName); + writeKV(out, "memory", comp->mem->ramSize >> 10); // bytes to KB if (comp->cpu->lib) { - fprintf(file, "cpu.type = %s@%s\n", comp->cpu->core->name, comp->cpu->libname); + writeKV(out, "cpu.type", comp->cpu->core->name, '@', comp->cpu->libname); } else { - fprintf(file, "cpu.type = %s\n", comp->cpu->core->name); + writeKV(out, "cpu.type", comp->cpu->core->name); } - fprintf(file, "cpu.frq = %i\n", int(comp->cpuFrq * 1e6)); - fprintf(file, "frq.mul = %f\n", comp->frqMul); - fprintf(file, "scrp.wait = %s\n", YESNO(comp->flgEM1)); - fprintf(file, "contio = %s\n", YESNO(comp->flgCNTI)); - fprintf(file, "contmem = %s\n", YESNO(comp->flgCNTM)); - - fprintf(file, "\n[ROMSET]\n\n"); - fprintf(file, "current = %s\n", prf->rsName.c_str()); - fprintf(file, "reset = "); + writeKV(out, "cpu.frq", int(comp->cpuFrq * 1e6)); + writeKV(out, "frq.mul", comp->frqMul); + writeKV(out, "scrp.wait", YESNO(comp->flgEM1)); + writeKV(out, "contio", YESNO(comp->flgCNTI)); + writeKV(out, "contmem", YESNO(comp->flgCNTM)); + + out << "\n[ROMSET]\n\n"; + writeKV(out, "current", prf->rsName); switch (comp->resbank) { - case RES_48: fprintf(file, "basic48\n"); break; - case RES_128: fprintf(file, "basic128\n"); break; - case RES_DOS: fprintf(file, "dos\n"); break; - case RES_SHADOW: fprintf(file, "shadow\n"); break; + case RES_48: writeKV(out, "reset", "basic48"); break; + case RES_128: writeKV(out, "reset", "basic128"); break; + case RES_DOS: writeKV(out, "reset", "dos"); break; + case RES_SHADOW: writeKV(out, "reset", "shadow"); break; } - fprintf(file, "\n[VIDEO]\n\n"); - fprintf(file, "geometry = %s\n", prf->layName.c_str()); - fprintf(file, "4t-border = %s\n", YESNO(comp->vid->brdstep & 0x06)); - fprintf(file, "ULAplus = %s\n", YESNO(comp->vid->ula->enabled)); - fprintf(file, "contPattern = %i\n", comp->vid->ula->conttype); - fprintf(file, "earlyTiming = %s\n", YESNO(comp->vid->ula->early)); - fprintf(file, "DDpal = %s\n", YESNO(comp->flgDDP)); - fprintf(file, "palette = %s\n", prf->palette.c_str()); - - fprintf(file, "\n[SOUND]\n\n"); - fprintf(file, "chip1 = %i\n", comp->ts->chipA->type); - fprintf(file, "chip1.stereo = %i\n", comp->ts->chipA->stereo); - fprintf(file, "chip1.frq = %f\n", comp->ts->chipA->frq); - fprintf(file, "chip2 = %i\n", comp->ts->chipB->type); - fprintf(file, "chip2.stereo = %i\n", comp->ts->chipB->stereo); - fprintf(file, "chip2.frq = %f\n", comp->ts->chipB->frq); - fprintf(file, "chip3 = %i\n", comp->ts->chipC->type); - fprintf(file, "chip3.stereo = %i\n", comp->ts->chipC->stereo); - fprintf(file, "chip3.frq = %f\n", comp->ts->chipC->frq); - fprintf(file, "ts.type = %i\n", comp->ts->type); - - fprintf(file, "gs = %s\n", YESNO(comp->gs->enable)); - fprintf(file, "gs.reset = %s\n", YESNO(comp->gs->stereo)); - fprintf(file, "gs.stereo = %i\n", comp->gs->stereo); - - fprintf(file, "soundrive_type = %i\n", comp->sdrv->type); - - fprintf(file, "saa = %s\n", YESNO(comp->saa->enabled)); - // fprintf(file, "saa.stereo = %s\n", YESNO(!comp->saa->mono)); - - fprintf(file, "\n[INPUT]\n\n"); - fprintf(file, "mouse = %s\n", YESNO(comp->mouse->enable)); - fprintf(file, "mouse.wheel = %s\n", YESNO(comp->mouse->hasWheel)); - fprintf(file, "mouse.swapButtons = %s\n", YESNO(comp->mouse->swapButtons)); - fprintf(file, "mouse.sensitivity = %f\n", comp->mouse->sensitivity); - fprintf(file, "mouse.pctype = %i\n", comp->mouse->pcmode); - fprintf(file, "joy.extbuttons = %s\n", YESNO(comp->joy->extbuttons)); - fprintf(file, "gamepad.map = %s\n", prf->jmapNameA.c_str()); - fprintf(file, "gamepad2.map = %s\n", prf->jmapNameB.c_str()); - fprintf(file, "kbd.scantab = %i\n", comp->keyb->pcmode); + out << "\n[VIDEO]\n\n"; + writeKV(out, "geometry", prf->layName); + writeKV(out, "4t-border", YESNO(comp->vid->brdstep & 0x06)); + writeKV(out, "ULAplus", YESNO(comp->vid->ula->enabled)); + writeKV(out, "contPattern", comp->vid->ula->conttype); + writeKV(out, "earlyTiming", YESNO(comp->vid->ula->early)); + writeKV(out, "DDpal", YESNO(comp->flgDDP)); + writeKV(out, "palette", prf->palette); + + out << "\n[SOUND]\n\n"; + writeKV(out, "chip1", comp->ts->chipA->type); + writeKV(out, "chip1.stereo", comp->ts->chipA->stereo); + writeKV(out, "chip1.frq", comp->ts->chipA->frq); + writeKV(out, "chip2", comp->ts->chipB->type); + writeKV(out, "chip2.stereo", comp->ts->chipB->stereo); + writeKV(out, "chip2.frq", comp->ts->chipB->frq); + writeKV(out, "chip3", comp->ts->chipC->type); + writeKV(out, "chip3.stereo", comp->ts->chipC->stereo); + writeKV(out, "chip3.frq", comp->ts->chipC->frq); + writeKV(out, "ts.type", comp->ts->type); + writeKV(out, "gs", YESNO(comp->gs->enable)); + writeKV(out, "gs.reset", YESNO(comp->gs->stereo)); + writeKV(out, "gs.stereo", comp->gs->stereo); + writeKV(out, "soundrive_type", comp->sdrv->type); + writeKV(out, "saa", YESNO(comp->saa->enabled)); + + out << "\n[INPUT]\n\n"; + writeKV(out, "mouse", YESNO(comp->mouse->enable)); + writeKV(out, "mouse.wheel", YESNO(comp->mouse->hasWheel)); + writeKV(out, "mouse.swapButtons", YESNO(comp->mouse->swapButtons)); + writeKV(out, "mouse.sensitivity", comp->mouse->sensitivity); + writeKV(out, "mouse.pctype", comp->mouse->pcmode); + writeKV(out, "joy.extbuttons", YESNO(comp->joy->extbuttons)); + writeKV(out, "gamepad.map", prf->jmapNameA); + writeKV(out, "gamepad2.map", prf->jmapNameB); + writeKV(out, "kbd.scantab", comp->keyb->pcmode); if ((prf->kmapName != "") && (prf->kmapName != "default")) - fprintf(file, "keymap = %s\n", prf->kmapName.c_str()); - - fprintf(file, "\n[TAPE]\n\n"); - fprintf(file, "path = %s\n", comp->tape->path ? comp->tape->path : ""); - fprintf(file, "speed = %i\n", comp->tape->speed); - - fprintf(file, "\n[DISK]\n\n"); - fprintf(file, "type = %i\n", comp->dif->type); - fprintf(file, "A = %s\n", getDiskString(comp->dif->fdc->flop[0]).c_str()); - fprintf(file, "B = %s\n", getDiskString(comp->dif->fdc->flop[1]).c_str()); - fprintf(file, "C = %s\n", getDiskString(comp->dif->fdc->flop[2]).c_str()); - fprintf(file, "D = %s\n", getDiskString(comp->dif->fdc->flop[3]).c_str()); - - fprintf(file, "\n[IDE]\n\n"); - fprintf(file, "iface = %i\n", comp->ide->type); - fprintf(file, "master.type = %i\n", comp->ide->master->type); - fprintf(file, "master.image = %s\n", comp->ide->master->image ? comp->ide->master->image : ""); - fprintf(file, "master.lba = %s\n", YESNO(comp->ide->master->hasLBA)); - fprintf(file, "slave.type = %i\n", comp->ide->slave->type); - fprintf(file, "slave.image = %s\n", comp->ide->slave->image ? comp->ide->slave->image : ""); - fprintf(file, "slave.lba = %s\n", YESNO(comp->ide->slave->hasLBA)); - - fprintf(file, "\n[SDC]\n\n"); - fprintf(file, "sdcimage = %s\n", comp->sdc->image ? comp->sdc->image : ""); - fprintf(file, "sdclock = %s\n", YESNO(comp->sdc->lock)); -// fprintf(file, "capacity = %i\n", comp->sdc->capacity); - - fprintf(file, "\n[SLOT]\n"); - fprintf(file, "type = %i\n",comp->slot->mapType); - fprintf(file, "path = %s\n", comp->slot->path ? comp->slot->path : ""); - - fclose(file); + writeKV(out, "keymap", prf->kmapName); + + out << "\n[TAPE]\n\n"; + writeKV(out, "path", orEmpty(comp->tape->path)); + writeKV(out, "speed", comp->tape->speed); + + out << "\n[DISK]\n\n"; + writeKV(out, "type", comp->dif->type); + writeKV(out, "A", getDiskString(comp->dif->fdc->flop[0])); + writeKV(out, "B", getDiskString(comp->dif->fdc->flop[1])); + writeKV(out, "C", getDiskString(comp->dif->fdc->flop[2])); + writeKV(out, "D", getDiskString(comp->dif->fdc->flop[3])); + + out << "\n[IDE]\n\n"; + writeKV(out, "iface", comp->ide->type); + writeKV(out, "master.type", comp->ide->master->type); + writeKV(out, "master.image", orEmpty(comp->ide->master->image)); + writeKV(out, "master.lba", YESNO(comp->ide->master->hasLBA)); + writeKV(out, "slave.type", comp->ide->slave->type); + writeKV(out, "slave.image", orEmpty(comp->ide->slave->image)); + writeKV(out, "slave.lba", YESNO(comp->ide->slave->hasLBA)); + + out << "\n[SDC]\n\n"; + writeKV(out, "sdcimage", orEmpty(comp->sdc->image)); + writeKV(out, "sdclock", YESNO(comp->sdc->lock)); + + out << "\n[SLOT]\n"; + writeKV(out, "type", comp->slot->mapType); + writeKV(out, "path", orEmpty(comp->slot->path)); return PSAVE_OK; } diff --git a/src/xcore/sound.cpp b/src/xcore/sound.cpp index 4510a5b4..5b6383fe 100644 --- a/src/xcore/sound.cpp +++ b/src/xcore/sound.cpp @@ -178,11 +178,7 @@ Uint32 sdl_timer_callback(Uint32 iv, void* ptr) { int null_open() { printf("NULL device opening...\n"); tid = SDL_AddTimer(20, sdl_timer_callback, NULL); -#ifdef HAVESDL1 - if (tid == NULL) { -#else if (tid < 0) { -#endif printf("Can't create SDL_Timer, syncronisation unavailable\n"); throw(0); } @@ -285,9 +281,7 @@ void sdlclose() { OutSys sndTab[] = { {xOutputNone,"NULL",&null_open,/*&null_play,*/&null_close}, -#if defined(HAVESDL1) || defined(HAVESDL2) {xOutputSDL,"SDL",&sdlopen,/*&sdlplay,*/&sdlclose}, -#endif {0,NULL,NULL,/*NULL,*/NULL} }; diff --git a/src/xcore/xcore.h b/src/xcore/xcore.h index 33e2daf8..f522bd9b 100644 --- a/src/xcore/xcore.h +++ b/src/xcore/xcore.h @@ -1,8 +1,15 @@ #pragma once +#include +#include +#include +#include +#include #include +#include #include #include +#include #if defined(__linux) || defined(__BSD) #include @@ -10,6 +17,9 @@ #include +#include + +#include #include #include #include @@ -39,16 +49,12 @@ #define xEventY position().y() #define xGlobalX globalPosition().x() #define xGlobalY globalPosition().y() -#elif QT_VERSION >= QT_VERSION_CHECK(5,0,0) +#else #define yDelta angleDelta().y() #define xEventX x() #define xEventY y() #define xGlobalX globalX() #define xGlobalY globalY() -#else - #define yDelta delta() - #define xEventX x() - #define xEventY y() #endif #if QT_VERSION >= QT_VERSION_CHECK(5,14,0) @@ -68,14 +74,145 @@ #include #define X_BackgroundRole Qt::BackgroundRole #define X_MidButton Qt::MiddleButton - typedef QSurfaceFormat QGLFormat; - typedef QOpenGLContext QGLContext; #else #include #define X_BackgroundRole Qt::BackgroundColorRole #define X_MidButton Qt::MidButton #endif +namespace fs = std::filesystem; + +// Which side of the search path an enumerated entry came from. Named so call +// sites don't have to interpret a bare bool. +enum class ResourceOrigin { User, System }; + +struct ResolvedEntry { + fs::path path; // full path on disk + fs::path name; // basename (or relative path for enumerateRecursive) + ResourceOrigin origin; // User (writable dir) or System (readonly dir) +}; + +// A resolved XDG search set for one shippable resource kind: one user-writable +// dir plus zero or more system dirs (from $XDG_{CONFIG,DATA}_DIRS on Linux; +// empty on Windows). Populated once in conf_init, then queried directly at +// call sites as `conf.path..tryFind(...)` etc. +struct ResourceDirs { + fs::path writable; // User-writable dir + std::vector readonly; // System dirs in search order + + // Search for a file by name: writable dir first, then each readonly dir. + // Returns nullopt if nothing exists. Use this when the caller wants to + // distinguish "not found anywhere" from "found but unusable". + std::optional tryFind(const fs::path &name) const { + const fs::path user = writable / name; + if (fs::exists(user)) return user; + for (const auto &ro : readonly) { + if (const fs::path p = ro / name; fs::exists(p)) return p; + } + return std::nullopt; + } + + // Convenience: falls back to the writable-dir path when nothing exists so + // callers that only want an error-message location get one for free. + fs::path find(const fs::path &name) const { + return tryFind(name).value_or(writable / name); + } + + // Higher-order enumerator: descends into subdirs of every search path, + // passing each candidate to the predicate. Accepted entries are returned + // tagged User (writable) or System (readonly). ResolvedEntry::name is the + // relative path from the search root (e.g. "zx48/48k.rom"). Symlinks are + // NOT followed; permission errors in subtrees are skipped. No de-dup: if + // the same basename exists on both sides, both entries appear. Results + // are sorted by relative path. + template + std::vector enumerateRecursive(Pred &&predicate) const { + std::vector out; + auto addFromDir = [&](const fs::path &dir, ResourceOrigin origin) { + std::error_code ec; + if (!fs::exists(dir, ec) || !fs::is_directory(dir, ec)) return; + const auto opts = fs::directory_options::skip_permission_denied; + fs::recursive_directory_iterator it(dir, opts, ec); + if (ec) return; + const fs::recursive_directory_iterator end; + while (it != end) { + std::error_code fec; + if (it->is_regular_file(fec) && predicate(it->path())) { + out.push_back({it->path(), + it->path().lexically_relative(dir), + origin}); + } + std::error_code iec; + it.increment(iec); + if (iec) break; + } + }; + addFromDir(writable, ResourceOrigin::User); + for (const auto &ro : readonly) addFromDir(ro, ResourceOrigin::System); + std::sort(out.begin(), out.end(), + [](const ResolvedEntry &a, const ResolvedEntry &b) { + return a.name < b.name; + }); + return out; + } +}; + +// Small adapter so callers don't have to write QString::fromStdString(p.string()) +// at every Qt boundary. fs::path::string() returns UTF-8 on all supported +// platforms, matching QString::fromStdString's input expectation. +inline QString toQString(const fs::path &p) { + return QString::fromStdString(p.string()); +} +// Overload for std::string so every Qt-string conversion in the xdg pipeline +// funnels through one helper instead of mixing fromStdString and toQString. +inline QString toQString(const std::string &s) { + return QString::fromStdString(s); +} + +// QString has no std::ostream inserter by default; add one so writeKV and +// direct `out << qstring` work uniformly. UTF-8 is used for consistency with +// fs::path::string() and toQString, which both use UTF-8. +inline std::ostream& operator<<(std::ostream &out, const QString &s) { + return out << s.toUtf8().constData(); +} + +// Helper for writing INI-style "key = value\n" entries to an ostream. Factors +// out the repeating pattern in saveConfig/prfSave where dozens of keys are +// dumped one per line. Variadic: any number of value arguments are emitted +// left-to-right (via a C++17 fold expression) between the "key = " prefix +// and the trailing newline. Stream flags (e.g. std::fixed/setprecision that +// the caller already set for %f-style formatting) are preserved because we +// write directly into the destination stream rather than an intermediate +// buffer. Works for anything with an operator<<(ostream, T), including +// const char*, std::string, int, char, YESNO() macro, fs::path, QString. +template +inline void writeKV(std::ostream &out, std::string_view key, const Args&... values) { + out << key << " = "; + (out << ... << values); + out << '\n'; +} + +// Predicate factory: returns a callable that matches a path if its extension +// is any of the given ones (leading dot included, e.g. ".txt"). Matching is +// case-insensitive so that e.g. "foo.ROM" matches ".rom". Captures the +// normalized extension list by value so the returned predicate owns its data. +inline auto byExtension(std::initializer_list exts) { + auto toLower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return std::tolower(c); }); + return s; + }; + std::vector owned; + owned.reserve(exts.size()); + for (const char *e : exts) owned.push_back(toLower(std::string{e})); + return [owned = std::move(owned), toLower](const fs::path &p) { + if (owned.empty()) return true; + const auto ext = toLower(p.extension().string()); + return std::any_of(owned.begin(), owned.end(), + [&](const std::string &e) { return ext == e; }); + }; +} + // common std::string getTimeString(int); @@ -86,7 +223,8 @@ void setFlagBit(bool, int*, int); bool str2bool(std::string); std::vector splitstr(std::string,const char*); std::pair splitline(std::string, char = '='); -void copyFile(const char*, const char*); +void copyFile(const fs::path &src, const fs::path &dst); +void copyResource(std::string_view src, const fs::path &dst); int toPower(int); int toLimits(int, int, int); @@ -481,16 +619,26 @@ struct xConfig { unsigned halt:1; } led; struct { - std::string confDir; - std::string confFile; - std::string romDir; - std::string prfDir; - std::string shdDir; - std::string palDir; - std::string plgDir; // so/dll/dynlib (experimental, works only for CPU) - std::string qssDir; // visual styles - std::string font; - std::string boot; + fs::path confDir; + fs::path confFile; + fs::path prfDir; + fs::path cacheDir; // $XDG_CACHE_HOME/samstyle/xpeccy — UI dock state and other + // derived, non-essential artifacts + fs::path stateDir; // $XDG_STATE_HOME/samstyle/xpeccy — persistent machine- + // authored state (cmos/nvram blobs) + fs::path prfStateDir;// stateDir / profiles — per-profile state subtree + + // Per-kind search sets, populated by applyPlatformRoots. Each is queried + // directly: `conf.path.rom.tryFind("1982.rom")`, etc. Adding a new kind + // is a field here + an initKind call in applyPlatformRoots. + ResourceDirs rom; // $XDG_DATA_HOME/.../roms + ResourceDirs shader; // $XDG_DATA_HOME/.../shaders + ResourceDirs palette; // $XDG_DATA_HOME/.../palettes + ResourceDirs pluginCpu; // $XDG_DATA_HOME/.../plugins/cpu + ResourceDirs style; // $XDG_DATA_HOME/.../styles + ResourceDirs keymap; // $XDG_CONFIG_HOME/.../keymaps + ResourceDirs gamepad; // $XDG_CONFIG_HOME/.../gamepads + ResourceDirs boot; // $XDG_DATA_HOME/.../boot (TRDOS boot loader) } path; struct { unsigned labels:1; @@ -505,6 +653,7 @@ struct xConfig { QPoint pos; QSize siz; } dbg; + }; extern xConfig conf; diff --git a/src/xgui/classes.cpp b/src/xgui/classes.cpp index 5355571a..ef7b4c79 100644 --- a/src/xgui/classes.cpp +++ b/src/xgui/classes.cpp @@ -1,4 +1,5 @@ #include "xgui.h" +#include "resources_ui.h" #include "../xcore/xcore.h" #include @@ -242,56 +243,25 @@ void xLabel::mousePressEvent(QMouseEvent* ev) { // xTreeBox xTreeBox::xTreeBox(QWidget *p):QComboBox(p) { - tree = new QTreeView; - mod = new QFileSystemModel; - setModel(mod); - setView(tree); - mod->setNameFilters(QStringList() << "*.rom" << "*.bin"); - mod->setReadOnly(true); - mod->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot); - mod->setNameFilterDisables(false); - tree->setEditTriggers(QTableView::NoEditTriggers); - tree->setSelectionBehavior(QAbstractItemView::SelectRows); - tree->setSelectionMode(QAbstractItemView::SingleSelection); - tree->header()->setVisible(false); -} - -void xTreeBox::setDir(QString dir) { - QModelIndex idx = mod->setRootPath(dir); - setRootModelIndex(idx); -} - -void xTreeBox::showPopup() { - for (int i = 1; i < mod->columnCount(); i++) { - tree->hideColumn(i); - } - QComboBox::showPopup(); } -void xTreeBox::hidePopup() { - QModelIndex idx = tree->selectionModel()->currentIndex(); - QFileInfo inf = mod->fileInfo(idx); - if (inf.isDir()) return; - QComboBox::hidePopup(); +void xTreeBox::setResource(const ResourceDirs &dirs, + std::initializer_list extensions) { + clear(); + setDuplicatesEnabled(true); + forEachResource(this, dirs, byExtension(extensions), + [this](const QIcon &icon, const ResolvedEntry &e) { + addItem(icon, toQString(e.name), toQString(e.path)); + }); } -void xTreeBox::setCurrentFile(QString path) { - path.prepend(SLSH); - path.prepend(mod->rootPath()); - QModelIndex idx = mod->index(path, 0); - if (!idx.isValid()) return; - QModelIndex x = rootModelIndex(); - setRootModelIndex(idx.parent()); - setModelColumn(0); - setCurrentIndex(idx.row()); - setRootModelIndex(x); - tree->setCurrentIndex(idx); +void xTreeBox::setCurrentFile(QString name) { + const int idx = findText(name); + if (idx >= 0) setCurrentIndex(idx); } QString xTreeBox::currentFile() { - QModelIndex idx = tree->selectionModel()->currentIndex(); - QFileInfo inf = mod->fileInfo(idx); - return mod->rootDirectory().relativeFilePath(inf.filePath()); + return currentText(); } // base table model diff --git a/src/xgui/debuga/debuger.cpp b/src/xgui/debuga/debuger.cpp index a9cc55b1..d8229158 100644 --- a/src/xgui/debuga/debuger.cpp +++ b/src/xgui/debuga/debuger.cpp @@ -588,9 +588,7 @@ DebugWin::DebugWin(QWidget* par):QMainWindow(par) { resize(minimumSize()); - QString path = conf.path.confDir.c_str(); - path.append("/debuga.layout"); - QFile file(path); + QFile file(toQString(conf.path.cacheDir / "debuga.layout")); if (file.open(QFile::ReadOnly)) { QByteArray state = file.readAll(); file.close(); @@ -600,9 +598,7 @@ DebugWin::DebugWin(QWidget* par):QMainWindow(par) { DebugWin::~DebugWin() { QByteArray state = saveState(); - QString path = conf.path.confDir.c_str(); - path.append("/debuga.layout"); - QFile file(path); + QFile file(toQString(conf.path.cacheDir / "debuga.layout")); if (file.open(QFile::WriteOnly)) { file.write(state); file.close(); diff --git a/src/xgui/debuga/debuger.h b/src/xgui/debuga/debuger.h index d477934d..086bd8ff 100644 --- a/src/xgui/debuga/debuger.h +++ b/src/xgui/debuga/debuger.h @@ -11,11 +11,7 @@ #include #include #include -#if QT_VERSION >= QT_VERSION_CHECK(5,4,0) #include -#else -#include -#endif #include "../labelist.h" #include "libxpeccy/spectrum.h" diff --git a/src/xgui/options/opt_gamepad.cpp b/src/xgui/options/opt_gamepad.cpp index ac9390c1..3100e152 100644 --- a/src/xgui/options/opt_gamepad.cpp +++ b/src/xgui/options/opt_gamepad.cpp @@ -1,6 +1,7 @@ #include "opt_gamepad.h" #include "../xgui.h" +#include "../resources_ui.h" #include "../../xcore/xcore.h" #include @@ -165,15 +166,13 @@ void xGamepadWidget::updateList() { } void xGamepadWidget::update(std::string mapname) { - QStringList lst; int i; updateList(); ui.sldDeadZone->setValue(gpad->deadZone()); - QDir dir(conf.path.confDir.c_str()); ui.cbMapFile->clear(); - lst = dir.entryList(QStringList() << "*.pad",QDir::Files,QDir::Name); - lst.prepend("none"); - ui.cbMapFile->addItems(lst); + ui.cbMapFile->addItem("none"); + fillComboFromResources(ui.cbMapFile, conf.path.gamepad, + byExtension({".pad"})); if (mapname.empty()) { ui.cbMapFile->setCurrentIndex(0); } else { diff --git a/src/xgui/options/opt_romset.cpp b/src/xgui/options/opt_romset.cpp index fbe6129a..331526f5 100644 --- a/src/xgui/options/opt_romset.cpp +++ b/src/xgui/options/opt_romset.cpp @@ -1,6 +1,5 @@ #include "opt_romset.h" -#include #include // ROMSET EDITOR @@ -10,21 +9,13 @@ std::string getRFText(QComboBox*); xRomsetEditor::xRomsetEditor(QWidget* par):QDialog(par) { ui.setupUi(this); - ui.cbFile->setDir(conf.path.romDir.c_str()); + ui.cbFile->setResource(conf.path.rom, {".rom", ".bin"}); connect(ui.rse_apply, SIGNAL(clicked()), this, SLOT(store())); connect(ui.rse_cancel, SIGNAL(clicked()), this, SLOT(hide())); } void xRomsetEditor::edit(xRomFile f) { xrf = f; -// QDir rdir(QString(conf.path.romDir.c_str())); -// QStringList rlst = rdir.entryList(QStringList() << "*.rom" << "*.bin", QDir::Files, QDir::Name); -// QString str; -// ui.cbFile->clear(); -// foreach(str, rlst) { -// ui.cbFile->addItem(str, str); -// } -// ui.cbFile->setCurrentIndex(rlst.indexOf(f.name.c_str())); ui.cbFile->setCurrentFile(f.name.c_str()); ui.cbFoffset->setValue(f.foffset); ui.cbFsize->setValue(f.fsize); @@ -33,7 +24,6 @@ void xRomsetEditor::edit(xRomFile f) { } void xRomsetEditor::store() { -// xrf.name = getRFText(ui.cbFile); xrf.name = std::string(ui.cbFile->currentFile().toLocal8Bit().data()); if (xrf.name.empty()) return; xrf.foffset = ui.cbFoffset->value(); @@ -82,59 +72,54 @@ QVariant xRomsetModel::headerData(int sect, Qt::Orientation ori, int role) const QVariant xRomsetModel::data(const QModelIndex& idx, int role) const { QVariant res; - QFileInfo inf; - std::string buf; if (!idx.isValid()) return res; - int row = idx.row(); - int col = idx.column(); + const int row = idx.row(); + const int col = idx.column(); if ((row < 0) || (row >= rowCount())) return res; if ((col < 0) || (col >= columnCount())) return res; - int rlsz = (int)rset->roms.size(); - switch (role) { - case Qt::DisplayRole: - switch(col) { - case 0: - if (row < rlsz) { - res = "ROM"; - } else if (row == rlsz) { - res = "GS"; - } else if (row == rlsz+1){ - res = "Font"; - } else { - res = "VGA"; - } - break; - case 1: - if (row < rlsz) { - res = QString(rset->roms[row].name.c_str()); - } else if (row == rlsz) { - res = QString(rset->gsFile.c_str()); - } else if (row == rlsz+1) { - res = QString(rset->fntFile.c_str()); - } else { - res = QString(rset->vBiosFile.c_str()); - } - break; - case 2: - if (row >= rlsz) break; - res = rset->roms[row].foffset; - break; - case 3: - if (row >= rlsz) break; - if (rset->roms[row].fsize > 0) { - res = rset->roms[row].fsize; - } else { - buf = conf.path.romDir + SLASH + rset->roms[row].name; - inf.setFile(tr(buf.c_str())); - res = QString("( %0 )").arg(inf.size() >> 10); - } - break; - case 4: - if (row >= rlsz) break; - res = rset->roms[row].roffset; - break; + const int rlsz = (int)rset->roms.size(); + if (role != Qt::DisplayRole) return res; + switch(col) { + case 0: + if (row < rlsz) { + res = "ROM"; + } else if (row == rlsz) { + res = "GS"; + } else if (row == rlsz+1){ + res = "Font"; + } else { + res = "VGA"; + } + break; + case 1: + if (row < rlsz) { + res = QString(rset->roms[row].name.c_str()); + } else if (row == rlsz) { + res = QString(rset->gsFile.c_str()); + } else if (row == rlsz+1) { + res = QString(rset->fntFile.c_str()); + } else { + res = QString(rset->vBiosFile.c_str()); } break; + case 2: + if (row >= rlsz) break; + res = rset->roms[row].foffset; + break; + case 3: + if (row >= rlsz) break; + if (rset->roms[row].fsize > 0) { + res = rset->roms[row].fsize; + } else if (const auto path = conf.path.rom.tryFind(rset->roms[row].name)) { + res = QString("( %0 )").arg(QFileInfo(toQString(*path)).size() >> 10); + } else { + res = "( missing )"; + } + break; + case 4: + if (row >= rlsz) break; + res = rset->roms[row].roffset; + break; } return res; } diff --git a/src/xgui/options/setupwin.cpp b/src/xgui/options/setupwin.cpp index eecd252e..5ba5ad16 100644 --- a/src/xgui/options/setupwin.cpp +++ b/src/xgui/options/setupwin.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -14,6 +16,7 @@ #include "filer.h" #include "setupwin.h" +#include "xgui/resources_ui.h" #include "xgui/xgui.h" #include "xcore/gamepad.h" #include "xcore/xcore.h" @@ -74,16 +77,12 @@ void fill_layout_list(QComboBox* box, QString txt = QString()) { } void fill_shader_list(QComboBox* box) { - QDir dir(conf.path.shdDir.c_str()); - QFileInfoList lst = dir.entryInfoList(QStringList() << "*.txt", QDir::Files, QDir::Name); - QFileInfo inf; box->clear(); box->addItem("none", 0); #if defined(USEOPENGL) if (conf.vid.shd_support) { - foreach(inf, lst) { - box->addItem(inf.fileName(), 1); - } + fillComboFromResources(box, conf.path.shader, + byExtension({".txt"}), QVariant(1)); box->setCurrentIndex(box->findText(conf.vid.shader.c_str())); if (box->currentIndex() < 0) box->setCurrentIndex(0); @@ -93,32 +92,18 @@ void fill_shader_list(QComboBox* box) { #endif } -/* void fill_palette_list(QComboBox* box) { - QDir dir(conf.path.palDir.c_str()); - QFileInfoList lst = dir.entryInfoList(QStringList() << "*.txt", QDir::Files, QDir::Name); - QFileInfo inf; box->clear(); - box->addItem("default"); // empty data (no filename = default pal) - foreach(inf, lst) { - box->addItem(inf.fileName(), inf.fileName()); // need data=text, cuz setRFIndex using data, not text - } - setRFIndex(box, conf.prof.cur->palette.c_str()); - if (box->currentIndex() < 0) - box->setCurrentIndex(0); + box->addItem("default"); + fillComboFromResources(box, conf.path.palette, byExtension({".txt", ".pal"})); + setRFIndex(box, conf.prof.cur->palette.c_str(), 0); } -*/ -void fillComboBox(QComboBox* box, QString path, QStringList filt, QString def = "", QString sel = "") { - QDir dir(path); - QFileInfoList lst = dir.entryInfoList(filt, QDir::Files, QDir::Name); - QFileInfo inf; +void fill_style_list(QComboBox* box) { box->clear(); - if (!def.isEmpty()) box->addItem(def); - foreach(inf, lst) { - box->addItem(inf.fileName(), inf.fileName()); - } - setRFIndex(box, sel, 0); + box->addItem("System"); + fillComboFromResources(box, conf.path.style, byExtension({".qss"})); + setRFIndex(box, conf.style.c_str(), 0); } // OBJECT @@ -140,10 +125,11 @@ void dbg_fill_chip_boxes(QComboBox* cbtype, QComboBox* cbstereo) { enum { roleName = Qt::UserRole, - roleLib + roleLib, + roleDir }; -void opt_fill_cpu_add(QComboBox* box, cpuCore* tab, QString libname) { +void opt_fill_cpu_add(QComboBox* box, cpuCore* tab, QString libname, QString libdir) { int i = 0; int cnt = box->count(); QString name; @@ -154,7 +140,8 @@ void opt_fill_cpu_add(QComboBox* box, cpuCore* tab, QString libname) { } box->addItem(name); box->setItemData(cnt, QString(tab[i].name), roleName); // name of cpu - box->setItemData(cnt, libname, roleLib); // name of library, empty for built-in + box->setItemData(cnt, libname, roleLib); // library filename, empty for built-in + box->setItemData(cnt, libdir, roleDir); // directory that library lives in cnt++; i++; } @@ -162,28 +149,26 @@ void opt_fill_cpu_add(QComboBox* box, cpuCore* tab, QString libname) { void opt_fill_cpu(QComboBox* box) { box->clear(); - opt_fill_cpu_add(box, cpuTab, ""); // buit-in - // add modules to ui.cbCpu, type=filename (not number) -> all files (so/dll/dylib) from ${plgDir}/cpu - QDir dir(QString(conf.path.plgDir.c_str()) + SLASH + "cpu"); - QStringList fnlst = dir.entryList(QStringList() << "*.*", QDir::Files, QDir::Name); + opt_fill_cpu_add(box, cpuTab, "", ""); // built-in + // Add CPU plugins from every dir in the PluginCpu search path. QLibrary lib; cpuCore* tab; cpuCore*(*foo)(); -// QString cn; - foreach(QString fn, fnlst) { - if (QLibrary::isLibrary(fn)) { - lib.setFileName(dir.absoluteFilePath(fn)); - if (lib.load()) { - foo = (cpuCore*(*)())(lib.resolve("getCore")); - if (foo) { - tab = foo(); - opt_fill_cpu_add(box, tab, fn); - } - lib.unload(); - } else { - qDebug() << lib.errorString(); - } + for (const auto &e : conf.path.pluginCpu.enumerateRecursive( + [](const fs::path&) { return true; })) { + const QString fn = toQString(e.name); + if (!QLibrary::isLibrary(fn)) continue; + lib.setFileName(toQString(e.path)); + if (!lib.load()) { + qDebug() << lib.errorString(); + continue; + } + foo = (cpuCore*(*)())(lib.resolve("getCore")); + if (foo) { + tab = foo(); + opt_fill_cpu_add(box, tab, fn, toQString(e.path.parent_path())); } + lib.unload(); } } @@ -261,8 +246,7 @@ SetupWin::SetupWin(QWidget* par):QDialog(par) { ui.labShader->setVisible(false); ui.cbShader->setVisible(false); #endif - //fill_palette_list(ui.cbPalPreset); - fillComboBox(ui.cbPalPreset, conf.path.palDir.c_str(), QStringList() << "*.txt" << "*.pal", "default", conf.prof.cur->palette.c_str()); + fill_palette_list(ui.cbPalPreset); paleditor = new xPalEditor(this); ui.cbNoflicMode->addItem("2-frames (fullscreen)", AF_2C_FULL); ui.cbNoflicMode->addItem("2-frames (adaptive)", AF_2C_ADAPTIVE); @@ -584,8 +568,7 @@ void SetupWin::start() { ui.ulaPlus->setChecked(comp->vid->ula->enabled); ui.cbDDp->setChecked(comp->flgDDP); fill_shader_list(ui.cbShader); - //fill_palette_list(ui.cbPalPreset); - fillComboBox(ui.cbPalPreset, conf.path.palDir.c_str(), QStringList() << "*.txt" << "*.pal", "default", conf.prof.cur->palette.c_str()); + fill_palette_list(ui.cbPalPreset); // sound ui.cbGS->setChecked(comp->gs->enable); ui.gsrbox->setChecked(comp->gs->reset); @@ -632,12 +615,6 @@ void SetupWin::start() { gpwid_b->update(prof->jmapNameB); // ui.sldDeadZone->setValue(conf.joy.gpad->deadZone()); // ui.cbGamepad->blockSignals(true); -// fillRFBox(ui.cbGamepad, conf.joy.gpad->getList()); -// setRFIndex(ui.cbGamepad, conf.joy.gpad->name()); // curName); -// ui.cbGamepad->blockSignals(false); -// padModel->update(); -// buildpadlist(); -// setRFIndex(ui.cbPadMap, conf.prof.cur->jmapNameA.c_str()); // flp ui.diskTypeBox->setCurrentIndex(ui.diskTypeBox->findData(comp->dif->type)); ui.bdtbox->setChecked(fdcFlag & FDC_FAST); @@ -734,7 +711,7 @@ void SetupWin::start() { setToolButtonColor(ui.tbDbgSelBG, "dbg.sel.bg","#c0e0c0"); setToolButtonColor(ui.tbDbgSelFG, "dbg.sel.txt","#000000"); setToolButtonColor(ui.tbDbgBrkFG, "dbg.brk.txt","#e08080"); - fillComboBox(ui.cbStyleSheet, conf.path.qssDir.c_str(), QStringList() << "*.qss", "System", conf.style.c_str()); + fill_style_list(ui.cbStyleSheet); // profiles ui.defstart->setChecked(conf.defProfile); buildproflist(); @@ -759,24 +736,13 @@ void SetupWin::apply() { // cpu QString name = ui.cbCpu->itemData(ui.cbCpu->currentIndex(), roleName).toString(); QString libn = ui.cbCpu->itemData(ui.cbCpu->currentIndex(), roleLib).toString(); + QString libd = ui.cbCpu->itemData(ui.cbCpu->currentIndex(), roleDir).toString(); if (libn.isEmpty()) { // built-in cpu_set_type(comp->cpu, name.toLocal8Bit().data(), NULL, NULL); } else { - std::string cpdir = conf.path.plgDir + SLASH + "cpu"; - cpu_set_type(comp->cpu, name.toLocal8Bit().data(), cpdir.c_str(), libn.toLocal8Bit().data()); - } -/* - int res = getRFIData(ui.cbCpu); - if (res < 0) { - std::string fpath = conf.path.plgDir + SLASH + "cpu"; - res = cpuSetLib(comp->cpu, fpath.c_str(), getRFSData(ui.cbCpu).toLocal8Bit().data()); - if (res < 0) { - shitHappens("Can't set CPU from library"); - } - } else { - cpuSetType(comp->cpu, getRFIData(ui.cbCpu)); + cpu_set_type(comp->cpu, name.toLocal8Bit().data(), + libd.toLocal8Bit().data(), libn.toLocal8Bit().data()); } -*/ compSetBaseFrq(comp, ui.sbFreq->value()); compSetTurbo(comp, ui.sbMult->value()); comp->flgEM1 = ui.scrpwait->isChecked(); @@ -1251,19 +1217,11 @@ void SetupWin::setRom(xRomFile f) { // lists -void SetupWin::buildpadlist() { -// QDir dir(conf.path.confDir.c_str()); -// QStringList lst = dir.entryList(QStringList() << "*.pad",QDir::Files,QDir::Name); -// fillRFBox(ui.cbPadMap, lst); -} - void SetupWin::buildkeylist() { - QDir dir(conf.path.confDir.c_str()); - QStringList lst = dir.entryList(QStringList() << "*.map",QDir::Files,QDir::Name); - dir.setPath(dir.path().append("/keymaps/")); - lst.append(dir.entryList(QStringList() << "*.map",QDir::Files,QDir::Name)); - lst.sort(); - fillRFBox(ui.keyMapBox,lst); + ui.keyMapBox->clear(); + ui.keyMapBox->addItem("none", ""); + fillComboFromResources(ui.keyMapBox, conf.path.keymap, + byExtension({".map"})); } struct xMemName { diff --git a/src/xgui/options/setupwin.h b/src/xgui/options/setupwin.h index a172774f..eb87d490 100644 --- a/src/xgui/options/setupwin.h +++ b/src/xgui/options/setupwin.h @@ -60,7 +60,6 @@ class SetupWin : public QDialog { void buildmenulist(); void buildkeylist(); void buildproflist(); - void buildpadlist(); signals: void closed(); diff --git a/src/xgui/resources_ui.h b/src/xgui/resources_ui.h new file mode 100644 index 00000000..25568487 --- /dev/null +++ b/src/xgui/resources_ui.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "../xcore/xcore.h" + +// Enumerate a resource set, precompute the user/system icon pair once, and +// invoke `add(icon, entry)` for every matching entry. This is the common spine +// shared by the combo, menu, and xTreeBox fillers — each specializes only in +// what they do per item. Icons are looked up via `styleSrc->style()` so the +// host widget decides the icon theme. +template +inline void forEachResource(QWidget *styleSrc, const ResourceDirs &dirs, + Pred &&pred, Adder &&add) { + const QIcon userIcon = styleSrc->style()->standardIcon(QStyle::SP_DirHomeIcon); + const QIcon sysIcon = styleSrc->style()->standardIcon(QStyle::SP_ComputerIcon); + for (const auto &e : dirs.enumerateRecursive(std::forward(pred))) { + add(e.origin == ResourceOrigin::User ? userIcon : sysIcon, e); + } +} + +// Populate a QComboBox with entries from a resource set, using icons to mark +// each entry as writable (user) or read-only (system). The predicate filters +// the enumeration (typically byExtension({...})). +// +// If `commonData` is invalid (the default), each entry stores its filename as +// user data. If `commonData` is set, every entry stores that value as user +// data — useful when the caller uses user data as a type tag (e.g. shader +// list distinguishing "none" vs "shader"). +template +inline void fillComboFromResources(QComboBox *box, const ResourceDirs &dirs, + Pred &&pred, + const QVariant &commonData = QVariant()) { + forEachResource(box, dirs, std::forward(pred), + [&](const QIcon &icon, const ResolvedEntry &e) { + const QString name = toQString(e.name); + box->addItem(icon, name, + commonData.isValid() ? commonData : QVariant(name)); + }); +} + +// Populate a QMenu with checkable actions from a resource set. Each action's +// data is the filename; the action whose filename equals `current` is marked +// checked. +template +inline void fillCheckableMenuFromResources(QMenu *menu, const ResourceDirs &dirs, + Pred &&pred, + const QString ¤t) { + forEachResource(menu, dirs, std::forward(pred), + [&](const QIcon &icon, const ResolvedEntry &e) { + const QString name = toQString(e.name); + QAction *act = menu->addAction(icon, name); + act->setData(name); + act->setCheckable(true); + act->setChecked(name == current); + }); +} diff --git a/src/xgui/xgui.h b/src/xgui/xgui.h index dd6f47a6..ecef2b22 100644 --- a/src/xgui/xgui.h +++ b/src/xgui/xgui.h @@ -1,8 +1,9 @@ #pragma once +#include + #include #include -#include #include #include #include @@ -10,6 +11,7 @@ #include #include +#include "../xcore/xcore.h" #include "classes.h" #if QT_VERSION < QT_VERSION_CHECK(6,0,0) @@ -85,14 +87,10 @@ class xTreeBox : public QComboBox { Q_OBJECT public: xTreeBox(QWidget* p = NULL); - void setDir(QString); + void setResource(const ResourceDirs &dirs, + std::initializer_list extensions); void setCurrentFile(QString); QString currentFile(); - private: - void showPopup(); - void hidePopup(); - QTreeView* tree; - QFileSystemModel* mod; }; enum {