diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index a3a425d42..8f34d13a2 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -13,7 +13,7 @@ jobs: - { name: "macOS", os: macos-12, - deps_cmdline: "brew install fluidsynth freeimage ftgl glm lua mpg123 sfml wxwidgets" + deps_cmdline: "brew install fluidsynth freeimage ftgl glm lua mpg123 sfml sqlite wxwidgets" } - { name: "Linux GCC", @@ -22,7 +22,7 @@ jobs: sudo apt-add-repository 'deb https://repos.codelite.org/wx3.2.0/ubuntu/ focal universe' && \ sudo apt update && sudo apt install \ libfluidsynth-dev libfreeimage-dev libftgl-dev libglm-dev libgtk-3-dev \ - liblua5.3-dev libmpg123-dev libsfml-dev libwxgtk3.2unofficial-dev" + liblua5.3-dev libmpg123-dev libsfml-dev libsqlite3-dev libwxgtk3.2unofficial-dev" } - { name: "Linux Clang", @@ -32,7 +32,7 @@ jobs: sudo apt-add-repository 'deb https://repos.codelite.org/wx3.2.0/ubuntu/ focal universe' && \ sudo apt update && sudo apt install \ libfluidsynth-dev libfreeimage-dev libftgl-dev libglm-dev libgtk-3-dev \ - liblua5.3-dev libmpg123-dev libsfml-dev libwxgtk3.2unofficial-dev" + liblua5.3-dev libmpg123-dev libsfml-dev libsqlite3-dev libwxgtk3.2unofficial-dev" } steps: diff --git a/.gitignore b/.gitignore index 9f00cb402..c318b3e63 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ !/dist/res !/dist/CMakeLists.txt !/dist/makebuild.ps1 +!/dist/7z_command.cmake /msvc/* !/msvc/resource.h diff --git a/cmake/unix.cmake b/cmake/unix.cmake index d889a849c..1ee1ec86b 100644 --- a/cmake/unix.cmake +++ b/cmake/unix.cmake @@ -111,6 +111,7 @@ if (NOT NO_LUA) endif() find_package(MPG123 REQUIRED) find_package(glm REQUIRED) +find_package(SQLite3 REQUIRED) include_directories( ${FREEIMAGE_INCLUDE_DIR} ${SFML_INCLUDE_DIR} @@ -180,6 +181,7 @@ target_link_libraries(slade ${LUA_LIBRARIES} ${MPG123_LIBRARIES} glm::glm + sqlite3 ) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION LESS 9) diff --git a/cmake/win_msvc.cmake b/cmake/win_msvc.cmake index 7bdd0625c..660c10e67 100644 --- a/cmake/win_msvc.cmake +++ b/cmake/win_msvc.cmake @@ -37,6 +37,8 @@ find_package(MPG123 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(SFML COMPONENTS system audio window network CONFIG REQUIRED) find_package(glm REQUIRED) +find_package(unofficial-sqlite3 CONFIG REQUIRED) +#find_package(xxHash CONFIG REQUIRED) # Include Search Paths --------------------------------------------------------- @@ -46,6 +48,7 @@ include_directories( . .. ../thirdparty/glad/include + ../thirdparty/SQLiteCpp/include ./Application ) @@ -97,6 +100,8 @@ target_link_libraries(slade sfml-network sfml-window glm::glm + unofficial::sqlite3::sqlite3 +# xxHash::xxhash ) if (NOT NO_LUA) diff --git a/dist/res/actions.cfg b/dist/res/actions.cfg index 883e322a8..bb9d79d6d 100644 --- a/dist/res/actions.cfg +++ b/dist/res/actions.cfg @@ -12,6 +12,7 @@ #include "actions/ptxt.cfg" // TextEntryPanel #include "actions/mapw.cfg" // MapEditorWindow #include "actions/scrm.cfg" // ScriptManagerWindow +#include "actions/alib.cfg" // LibraryPanel // MapEntryPanel action pmap_open_text diff --git a/dist/res/actions/alib.cfg b/dist/res/actions/alib.cfg new file mode 100644 index 000000000..f8b3e13d6 --- /dev/null +++ b/dist/res/actions/alib.cfg @@ -0,0 +1,21 @@ + +action alib_open +{ + text = "Open"; + icon = "open"; + help_text = "Open the seleted archive(s)"; +} + +action alib_remove +{ + text = "Remove"; + icon = "delete"; + help_text = "Remove the selected archive(s) from the library"; +} + +action alib_run +{ + text = "Run"; + icon = "run"; + help_text = "Run the selected archive"; +} diff --git a/dist/res/actions/main.cfg b/dist/res/actions/main.cfg index cb6052495..d94d00932 100644 --- a/dist/res/actions/main.cfg +++ b/dist/res/actions/main.cfg @@ -32,7 +32,7 @@ action main_setbra action main_preferences { - text = "&Preferences..."; + text = "&Preferences"; icon = "settings"; help_text = "Setup SLADE options and preferences"; custom_wx_id = 5022; // wxID_PREFERENCES @@ -100,7 +100,7 @@ action main_about action main_updatecheck { - text = "Check for Updates..."; + text = "Check for Updates"; help_text = "Check online for updates"; icon = "up"; } @@ -111,3 +111,10 @@ action main_runscript icon = "script"; help_text = "Open the Script Manager to write/run a SLADE script"; } + +action main_showlibrary +{ + text = "Archive Library"; + icon = "library"; + help_text = "Open the SLADE Archive Library"; +} diff --git a/dist/res/config/archive_formats.cfg b/dist/res/config/archive_formats.cfg index b763d8253..ca0bfbd48 100644 --- a/dist/res/config/archive_formats.cfg +++ b/dist/res/config/archive_formats.cfg @@ -13,8 +13,8 @@ archive_formats extensions { - wad = "WAD"; - iwad = "IWAD"; + wad = "Doom WAD"; + iwad = "Doom IWAD"; dta = "SRB2 DTA"; gwa = "GL-Nodes WAD"; hwa = "EDGE HWA"; @@ -56,7 +56,7 @@ archive_formats { name = "Quake BSP"; entry_format = "archive_bsp"; - extensions { bsp = "BSP"; } + extensions { bsp = "Quake BSP"; } } bz2 @@ -209,7 +209,7 @@ archive_formats max_name_length = 16; names_extensions = false; entry_format = "archive_wad2"; - extensions { wad = "WAD2"; } + extensions { wad = "Quake WAD2"; } } wadj @@ -220,7 +220,7 @@ archive_formats max_name_length = 8; entry_format = "archive_wadj"; prefer_uppercase = true; - extensions { wad = "WAD"; } + extensions { wad = "Jaguar Doom WAD"; } } wolf @@ -249,6 +249,6 @@ archive_formats max_name_length = 120; supports_dirs = true; entry_format = "archive_sin"; - extensions { sin = "SiN"; } + extensions { sin = "SiN File"; } } } diff --git a/dist/res/database/tables/archive_bookmark.sql b/dist/res/database/tables/archive_bookmark.sql new file mode 100644 index 000000000..22896b635 --- /dev/null +++ b/dist/res/database/tables/archive_bookmark.sql @@ -0,0 +1,12 @@ +BEGIN; + +CREATE TABLE archive_bookmark ( + archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE, + entry_id INTEGER, + UNIQUE ( + archive_id, + entry_id + ) +); + +COMMIT; diff --git a/dist/res/database/tables/archive_entry.sql b/dist/res/database/tables/archive_entry.sql new file mode 100644 index 000000000..846ad5079 --- /dev/null +++ b/dist/res/database/tables/archive_entry.sql @@ -0,0 +1,24 @@ +BEGIN; + +CREATE TABLE archive_entry ( + archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE, + id INTEGER, + path TEXT, + [index] INTEGER, + name TEXT, + size INTEGER, + hash TEXT, + type_id TEXT, + CONSTRAINT primary_key UNIQUE ( + archive_id, + id + ) +); + +CREATE INDEX entry_match ON archive_entry ( + name, + path, + hash +); + +COMMIT; diff --git a/dist/res/database/tables/archive_entry_property.sql b/dist/res/database/tables/archive_entry_property.sql new file mode 100644 index 000000000..23a3a2045 --- /dev/null +++ b/dist/res/database/tables/archive_entry_property.sql @@ -0,0 +1,16 @@ +BEGIN; + +CREATE TABLE archive_entry_property ( + archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE, + entry_id INTEGER, + [key] TEXT, + value_type INTEGER, + value NONE, + UNIQUE ( + archive_id, + entry_id, + [key] + ) +); + +COMMIT; diff --git a/dist/res/database/tables/archive_file.sql b/dist/res/database/tables/archive_file.sql new file mode 100644 index 000000000..1a3aef3bd --- /dev/null +++ b/dist/res/database/tables/archive_file.sql @@ -0,0 +1,14 @@ +BEGIN; + +CREATE TABLE archive_file ( + id INTEGER PRIMARY KEY, + path TEXT UNIQUE, -- Path to the archive file on disk (or directory), must be unique in the table + size INTEGER, -- Size of the file on disk (in bytes) + hash TEXT, -- Hash (xxHash 128-bit) of the file data on disk + format_id TEXT, -- Archive format id (eg. 'wad') + last_opened DATETIME, -- Time the archive was last opened in SLADE + last_modified DATETIME, -- Modified time of the file on disk + parent_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE -- For archives within archives, the id of this archive's parent +); + +COMMIT; diff --git a/dist/res/database/tables/archive_map.sql b/dist/res/database/tables/archive_map.sql new file mode 100644 index 000000000..b88b0d95d --- /dev/null +++ b/dist/res/database/tables/archive_map.sql @@ -0,0 +1,14 @@ +BEGIN; + +CREATE TABLE archive_map ( + archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE, + header_entry_id INTEGER, + name TEXT, + format INTEGER, + UNIQUE ( + archive_id, + header_entry_id + ) +); + +COMMIT; diff --git a/dist/res/database/tables/archive_map_config.sql b/dist/res/database/tables/archive_map_config.sql new file mode 100644 index 000000000..ba200f8fa --- /dev/null +++ b/dist/res/database/tables/archive_map_config.sql @@ -0,0 +1,9 @@ +BEGIN; + +CREATE TABLE archive_map_config ( + archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE, + game TEXT, + port TEXT +); + +COMMIT; diff --git a/dist/res/database/tables/archive_run_config.sql b/dist/res/database/tables/archive_run_config.sql new file mode 100644 index 000000000..db52578fa --- /dev/null +++ b/dist/res/database/tables/archive_run_config.sql @@ -0,0 +1,12 @@ +BEGIN; + +CREATE TABLE archive_run_config ( + archive_id INTEGER PRIMARY KEY + REFERENCES archive_file (id) ON DELETE CASCADE, + executable_id TEXT, + run_config INTEGER, + run_extra TEXT, + iwad_path TEXT +); + +COMMIT; diff --git a/dist/res/database/tables/archive_ui_config.sql b/dist/res/database/tables/archive_ui_config.sql new file mode 100644 index 000000000..c7d1bf001 --- /dev/null +++ b/dist/res/database/tables/archive_ui_config.sql @@ -0,0 +1,18 @@ +BEGIN; + +CREATE TABLE archive_ui_config ( + archive_id INTEGER PRIMARY KEY + REFERENCES archive_file (id) ON DELETE CASCADE, + elist_index_visible BOOLEAN, + elist_index_width INTEGER, + elist_name_width INTEGER, + elist_size_visible BOOLEAN, + elist_size_width INTEGER, + elist_type_visible BOOLEAN, + elist_type_width INTEGER, + elist_sort_column TEXT, + elist_sort_descending BOOLEAN, + splitter_position INTEGER +); + +COMMIT; diff --git a/dist/res/database/tables/db_info.sql b/dist/res/database/tables/db_info.sql new file mode 100644 index 000000000..5bec9d072 --- /dev/null +++ b/dist/res/database/tables/db_info.sql @@ -0,0 +1,7 @@ +BEGIN; + +CREATE TABLE db_info ( + version INTEGER +); + +COMMIT; diff --git a/dist/res/database/tables/session.sql b/dist/res/database/tables/session.sql new file mode 100644 index 000000000..e382284ec --- /dev/null +++ b/dist/res/database/tables/session.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE session ( + id INTEGER PRIMARY KEY, + opened_time DATETIME, + closed_time DATETIME, + version_major INTEGER, + version_minor INTEGER, + version_revision INTEGER, + version_beta INTEGER +); + +COMMIT; diff --git a/dist/res/database/tables/ui_state.sql b/dist/res/database/tables/ui_state.sql new file mode 100644 index 000000000..b5a470774 --- /dev/null +++ b/dist/res/database/tables/ui_state.sql @@ -0,0 +1,8 @@ +BEGIN; + +CREATE TABLE ui_state ( + name TEXT PRIMARY KEY, + value NONE +); + +COMMIT; diff --git a/dist/res/database/tables/window_info.sql b/dist/res/database/tables/window_info.sql new file mode 100644 index 000000000..e812063d2 --- /dev/null +++ b/dist/res/database/tables/window_info.sql @@ -0,0 +1,12 @@ +BEGIN; + +CREATE TABLE window_info ( + window_id TEXT PRIMARY KEY + NOT NULL, + left INTEGER, + top INTEGER, + width INTEGER, + height INTEGER +); + +COMMIT; diff --git a/dist/res/database/tables/window_layout.sql b/dist/res/database/tables/window_layout.sql new file mode 100644 index 000000000..dd66b44a0 --- /dev/null +++ b/dist/res/database/tables/window_layout.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE window_layout ( + window_id TEXT, + component TEXT, + layout TEXT, + UNIQUE ( + window_id, + component + ) +); + +COMMIT; diff --git a/dist/res/database/views/archive_library_list.sql b/dist/res/database/views/archive_library_list.sql new file mode 100644 index 000000000..7416ae3a1 --- /dev/null +++ b/dist/res/database/views/archive_library_list.sql @@ -0,0 +1,26 @@ +BEGIN; + +CREATE VIEW archive_library_list AS + SELECT archive_file.id, + archive_file.path, + archive_file.size, + archive_file.format_id, + archive_file.last_opened, + archive_file.last_modified, + archive_file.parent_id, + ( + SELECT COUNT( * ) + FROM archive_entry + WHERE archive_entry.archive_id = archive_file.id + ) + AS entry_count, + ( + SELECT COUNT( * ) + FROM archive_map + WHERE archive_map.archive_id = archive_file.id + ) + AS map_count + FROM archive_file + ORDER BY archive_file.path COLLATE NOCASE ASC; + +COMMIT; diff --git a/dist/res/icons.cfg b/dist/res/icons.cfg index e369ab1ab..a6adadcd4 100644 --- a/dist/res/icons.cfg +++ b/dist/res/icons.cfg @@ -41,6 +41,7 @@ general icon_svg importfiles = "icons/general/importfiles.svg"; icon_svg importdir = "icons/general/importdir.svg"; icon_svg left = "icons/general/left.svg"; + icon_svg library = "icons/general/library.svg"; icon_svg lightbulb = "icons/general/lightbulb.svg"; icon_svg linedraw = "icons/general/linedraw.svg"; icon_svg lines = "icons/general/lines.svg"; diff --git a/dist/res/icons/general/library.svg b/dist/res/icons/general/library.svg new file mode 100644 index 000000000..cf011110e --- /dev/null +++ b/dist/res/icons/general/library.svg @@ -0,0 +1,11 @@ + + + diff --git a/msvc/ThirdPartyLib.vcxproj b/msvc/ThirdPartyLib.vcxproj index 4ea6325f6..4e16244e1 100644 --- a/msvc/ThirdPartyLib.vcxproj +++ b/msvc/ThirdPartyLib.vcxproj @@ -110,6 +110,19 @@ + + + + + + + + + + + + + @@ -702,6 +715,14 @@ + + + + + + + + @@ -764,25 +785,25 @@ false - $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include + $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include ..\build\thirdparty\$(Platform)\$(Configuration)\ ..\build\lib\$(Platform)\$(Configuration)\ true - $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include + $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include ..\build\thirdparty\$(Configuration)\ ..\build\lib\$(Configuration)\ true - $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include + $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include ..\build\thirdparty\$(Platform)\$(Configuration)\ ..\build\lib\$(Platform)\$(Configuration)\ false - $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include + $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include ..\build\thirdparty\$(Configuration)\ ..\build\lib\$(Configuration)\ diff --git a/src/Application/App.cpp b/src/Application/App.cpp index 39dc10355..52e68cc6f 100644 --- a/src/Application/App.cpp +++ b/src/Application/App.cpp @@ -36,6 +36,7 @@ #include "Archive/ArchiveManager.h" #include "Archive/EntryType/EntryDataFormat.h" #include "Archive/EntryType/EntryType.h" +#include "Database/Database.h" #include "Game/Game.h" #include "Game/SpecialPreset.h" #include "General/Clipboard.h" @@ -43,13 +44,13 @@ #include "General/Console.h" #include "General/Executables.h" #include "General/KeyBind.h" -#include "General/Misc.h" #include "General/ResourceManager.h" #include "General/SAction.h" #include "General/UI.h" #include "Graphics/Icons.h" #include "Graphics/Palette/PaletteManager.h" #include "Graphics/SImage/SIFormat.h" +#include "Library/Library.h" #include "MainEditor/MainEditor.h" #include "MapEditor/NodeBuilders.h" #include "OpenGL/GLTexture.h" @@ -60,6 +61,7 @@ #include "TextEditor/TextStyle.h" #include "UI/Dialogs/SetupWizard/SetupWizardDialog.h" #include "UI/SBrush.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/StringUtils.h" #include "Utility/Tokenizer.h" @@ -115,7 +117,6 @@ ResourceManager resource_manager; CVAR(Int, temp_location, 0, CVar::Flag::Save) CVAR(String, temp_location_custom, "", CVar::Flag::Save) -CVAR(Bool, setup_wizard_run, false, CVar::Flag::Save) // ---------------------------------------------------------------------------- @@ -214,7 +215,7 @@ bool initDirectories() { if (!wxMkdir(dir_user)) { - wxMessageBox(wxString::Format("Unable to create user directory \"%s\"", dir_user), "Error", wxICON_ERROR); + global::error = fmt::format("Unable to create user directory \"{}\"", dir_user); return false; } } @@ -225,7 +226,7 @@ bool initDirectories() { if (!wxMkdir(dir_temp)) { - wxMessageBox(wxString::Format("Unable to create temp directory \"%s\"", dir_temp), "Error", wxICON_ERROR); + global::error = fmt::format("Unable to create temp directory \"{}\"", dir_temp); return false; } } @@ -288,18 +289,9 @@ void readConfigFile() tz.adv(); // Skip ending } } - // Read recent files list + // Read (pre-3.3.0) recent files list if (tz.advIf("recent_files", 2)) - { - while (!tz.checkOrEnd("}")) - { - auto path = wxString::FromUTF8(tz.current().text.c_str()); - archive_manager.addRecentFile(wxutil::strToView(path)); - tz.adv(); - } - - tz.adv(); // Skip ending } - } + library::readPre330RecentFiles(tz); // Read keybinds if (tz.advIf("keys", 2)) @@ -331,10 +323,6 @@ void readConfigFile() tz.adv(); // Skip ending } } - // Read window size/position info - if (tz.advIf("window_info", 2)) - misc::readWindowInfo(tz); - // Next token tz.adv(); } @@ -422,6 +410,14 @@ ResourceManager& app::resources() return resource_manager; } +// ----------------------------------------------------------------------------- +// Returns the program resource archive (ie. slade.pk3) +// ----------------------------------------------------------------------------- +Archive* app::programResource() +{ + return archive_manager.programResourceArchive(); +} + // ----------------------------------------------------------------------------- // Returns the number of ms elapsed since the application was started // ----------------------------------------------------------------------------- @@ -479,14 +475,17 @@ bool app::init(const vector& args, double ui_scale) archive_manager.init(); if (!archive_manager.resArchiveOK()) { - wxMessageBox( - "Unable to find slade.pk3, make sure it exists in the same directory as the " - "SLADE executable", - "Error", - wxICON_ERROR); + global::error = "Unable to find slade.pk3, make sure it exists in the same directory as the SLADE executable"; return false; } + // Init database + if (!database::init()) + return false; + + // Init library + library::init(); + // Init SActions SAction::setBaseWxId(26000); SAction::initActions(); @@ -575,11 +574,11 @@ bool app::init(const vector& args, double ui_scale) log::info("SLADE Initialisation OK"); // Show Setup Wizard if needed - if (!setup_wizard_run) + if (!ui::getStateBool("SetupWizardRun")) { SetupWizardDialog dlg(maineditor::windowWx()); dlg.ShowModal(); - setup_wizard_run = true; + ui::saveStateBool("SetupWizardRun", true); maineditor::windowWx()->Update(); maineditor::windowWx()->Refresh(); } @@ -615,10 +614,10 @@ void app::saveConfigFile() return; // Write cfg header - file.Write("/*****************************************************\n"); - file.Write(" * SLADE Configuration File\n"); - file.Write(" * Don't edit this unless you know what you're doing\n"); - file.Write(" *****************************************************/\n\n"); + file.Write("// ----------------------------------------------------\n"); + file.Write(wxString::Format("// SLADE v%s Configuration File\n", version_num.toString())); + file.Write("// Don't edit this unless you know what you're doing\n"); + file.Write("// ----------------------------------------------------\n\n"); // Write cvars file.Write(CVar::writeAll(), wxConvUTF8); @@ -633,11 +632,14 @@ void app::saveConfigFile() } file.Write("}\n"); - // Write recent files list (in reverse to keep proper order when reading back) - file.Write("\nrecent_files\n{\n"); - for (int a = archive_manager.numRecentFiles() - 1; a >= 0; a--) + // Write recent files + // This isn't used in 3.3.0+, but we'll write them anyway for backwards-compatibility with previous versions + // (will be removed eventually, perhaps in 3.4.0) + auto recent_files = library::recentFiles(); + file.Write("\n// Recent Files (for backwards compatibility with pre-3.3.0 SLADE)\nrecent_files\n{\n"); + for (int i = recent_files.size() - 1; i >= 0; --i) { - auto path = archive_manager.recentFile(a); + auto path = recent_files[i]; std::replace(path.begin(), path.end(), '\\', '/'); file.Write(wxString::Format("\t\"%s\"\n", path), wxConvUTF8); } @@ -657,13 +659,10 @@ void app::saveConfigFile() file.Write(executables::writePaths()); file.Write("}\n"); - // Write window info - file.Write("\nwindow_info\n{\n"); - misc::writeWindowInfo(file); - file.Write("}\n"); - // Close configuration file - file.Write("\n// End Configuration File\n\n"); + file.Write("\n// ----------------------------------------------------\n"); + file.Write("// End Configuration File\n"); + file.Write("// ----------------------------------------------------\n"); } // ----------------------------------------------------------------------------- @@ -727,6 +726,9 @@ void app::exit(bool save_config) // Close DUMB dumb_exit(); + // Close program database + database::close(); + // Exit wx Application wxGetApp().Exit(); } diff --git a/src/Application/App.h b/src/Application/App.h index 1055c11d8..baff38b15 100644 --- a/src/Application/App.h +++ b/src/Application/App.h @@ -4,6 +4,7 @@ namespace slade { +class Archive; class ArchiveManager; class Console; class PaletteManager; @@ -20,6 +21,7 @@ namespace app ArchiveManager& archiveManager(); Clipboard& clipboard(); ResourceManager& resources(); + Archive* programResource(); bool init(const vector& args, double ui_scale = 1.); void saveConfigFile(); diff --git a/src/Application/SLADEWxApp.cpp b/src/Application/SLADEWxApp.cpp index dbd9aa0b4..597bc86b3 100644 --- a/src/Application/SLADEWxApp.cpp +++ b/src/Application/SLADEWxApp.cpp @@ -83,7 +83,6 @@ int win_version_minor = 0; string current_action; bool update_check_message_box = false; -CVAR(String, dir_last, "", CVar::Flag::Save) CVAR(Bool, update_check, true, CVar::Flag::Save) CVAR(Bool, update_check_beta, false, CVar::Flag::Save) @@ -485,7 +484,10 @@ bool SLADEWxApp::OnInit() try { if (!app::init(args, ui_scale)) + { + wxMessageBox(global::error, "SLADE Initialization Error", wxICON_ERROR); return false; + } } catch (const std::exception& ex) { diff --git a/src/Archive/Archive.cpp b/src/Archive/Archive.cpp index 0910a5ec4..0c195bfa4 100644 --- a/src/Archive/Archive.cpp +++ b/src/Archive/Archive.cpp @@ -220,7 +220,7 @@ string Archive::filename(bool full) const if (parentArchive()) parent_archive = parentArchive()->filename(false) + "/"; - return parent_archive.append(strutil::Path::fileNameOf(parent->name(), false)); + return parent_archive.append(strutil::Path::fileNameOf(parent->name())); } return full ? filename_ : string{ strutil::Path::fileNameOf(filename_) }; @@ -254,7 +254,7 @@ string Archive::formatId() const // Reads an archive from disk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool Archive::open(string_view filename) +bool Archive::open(string_view filename, bool detect_types) { // Update filename before opening const auto backupname = filename_; @@ -262,7 +262,7 @@ bool Archive::open(string_view filename) // Open via format handler const sf::Clock timer; - if (format_handler_->open(*this, filename)) + if (format_handler_->open(*this, filename, detect_types)) { log::info(2, "Archive::open took {}ms", timer.getElapsedTime().asMilliseconds()); file_modified_ = fileutil::fileModifiedTime(filename); @@ -281,14 +281,14 @@ bool Archive::open(string_view filename) // Reads an archive from an ArchiveEntry // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool Archive::open(ArchiveEntry* entry) +bool Archive::open(ArchiveEntry* entry, bool detect_types) { - return format_handler_->open(*this, entry); + return format_handler_->open(*this, entry, detect_types); } -bool Archive::open(const MemChunk& mc) +bool Archive::open(const MemChunk& mc, bool detect_types) { - return format_handler_->open(*this, mc); + return format_handler_->open(*this, mc, detect_types); } bool Archive::write(MemChunk& mc) @@ -930,7 +930,7 @@ MapDesc Archive::mapDesc(ArchiveEntry* maphead) // Returns the MapDesc information about all maps in the Archive. // To be implemented in Archive sub-classes. // ----------------------------------------------------------------------------- -vector Archive::detectMaps() +vector Archive::detectMaps() const { return format_handler_->detectMaps(*this); } @@ -938,7 +938,7 @@ vector Archive::detectMaps() // ----------------------------------------------------------------------------- // Returns the namespace of the entry at [index] within [dir] // ----------------------------------------------------------------------------- -string Archive::detectNamespace(unsigned index, ArchiveDir* dir) +string Archive::detectNamespace(unsigned index, ArchiveDir* dir) const { return format_handler_->detectNamespace(*this, index, dir); } @@ -946,7 +946,7 @@ string Archive::detectNamespace(unsigned index, ArchiveDir* dir) // ----------------------------------------------------------------------------- // Returns the namespace that [entry] is within // ----------------------------------------------------------------------------- -string Archive::detectNamespace(ArchiveEntry* entry) +string Archive::detectNamespace(ArchiveEntry* entry) const { return format_handler_->detectNamespace(*this, entry); } @@ -1039,14 +1039,16 @@ void Archive::blockModificationSignals(bool block) // ----------------------------------------------------------------------------- // Detects the type of all entries in the archive // ----------------------------------------------------------------------------- -void Archive::detectAllEntryTypes() const +void Archive::detectAllEntryTypes(bool show_in_splash_window) const { auto entries = dir_root_->allEntries(); auto n_entries = entries.size(); - ui::setSplashProgressMessage("Detecting entry types"); + if (show_in_splash_window) + ui::setSplashProgressMessage("Detecting entry types"); for (size_t i = 0; i < n_entries; i++) { - ui::setSplashProgress(i, n_entries); + if (show_in_splash_window) + ui::setSplashProgress(i, n_entries); EntryType::detectEntryType(*entries[i]); entries[i]->setState(EntryState::Unmodified); } diff --git a/src/Archive/Archive.h b/src/Archive/Archive.h index 8ae901e85..81ada68d5 100644 --- a/src/Archive/Archive.h +++ b/src/Archive/Archive.h @@ -48,9 +48,11 @@ class Archive bool isReadOnly() const { return read_only_; } bool isWritable() const; time_t fileModifiedTime() const { return file_modified_; } + int64_t libraryId() const { return library_id_; } void setModified(bool modified); void setFilename(string_view filename) { filename_ = filename; } + void setLibraryId(int64_t library_id) const { library_id_ = library_id; } // Entry retrieval/info bool checkEntry(const ArchiveEntry* entry) const; @@ -71,9 +73,9 @@ class Archive string formatId() const; // Opening - bool open(string_view filename); // Open from File - bool open(ArchiveEntry* entry); // Open from ArchiveEntry - bool open(const MemChunk& mc); // Open from MemChunk + bool open(string_view filename, bool detect_types); // Open from File + bool open(ArchiveEntry* entry, bool detect_types); // Open from ArchiveEntry + bool open(const MemChunk& mc, bool detect_types); // Open from MemChunk // Writing/Saving bool write(MemChunk& mc); // Write to MemChunk @@ -122,9 +124,10 @@ class Archive // Detection MapDesc mapDesc(ArchiveEntry* maphead); - vector detectMaps(); - string detectNamespace(ArchiveEntry* entry); - string detectNamespace(unsigned index, ArchiveDir* dir = nullptr); + vector detectMaps() const; + string detectNamespace(ArchiveEntry* entry) const; + string detectNamespace(unsigned index, ArchiveDir* dir = nullptr) const; + void detectAllEntryTypes(bool show_in_splash_window = true) const; // Search ArchiveEntry* findFirst(ArchiveSearchOptions& options); @@ -156,14 +159,12 @@ class Archive bool read_only_ = false; // If true, the archive cannot be modified time_t file_modified_ = 0; - // Helpers - void detectAllEntryTypes() const; - private: bool modified_ = true; shared_ptr dir_root_; Signals signals_; unique_ptr format_handler_; + mutable int64_t library_id_ = -1; }; // Simple class that will block and unblock modification signals for an archive via RAII @@ -178,5 +179,4 @@ class ArchiveModSignalBlocker private: Archive* archive_; }; - } // namespace slade diff --git a/src/Archive/ArchiveDir.cpp b/src/Archive/ArchiveDir.cpp index a6c2bcd97..b92bbbab8 100644 --- a/src/Archive/ArchiveDir.cpp +++ b/src/Archive/ArchiveDir.cpp @@ -134,7 +134,7 @@ void ArchiveDir::setArchive(Archive* archive) // Returns the index of [entry] within this directory, or -1 if the entry // doesn't exist // ----------------------------------------------------------------------------- -int ArchiveDir::entryIndex(ArchiveEntry* entry, size_t startfrom) const +int ArchiveDir::entryIndex(const ArchiveEntry* entry, size_t startfrom) const { // Check entry was given if (!entry) diff --git a/src/Archive/ArchiveDir.h b/src/Archive/ArchiveDir.h index 678294a22..85126f003 100644 --- a/src/Archive/ArchiveDir.h +++ b/src/Archive/ArchiveDir.h @@ -34,7 +34,7 @@ class ArchiveDir shared_ptr sharedEntry(string_view name, bool cut_ext = false) const; shared_ptr sharedEntry(const ArchiveEntry* entry) const; unsigned numEntries(bool inc_subdirs = false) const; - int entryIndex(ArchiveEntry* entry, size_t startfrom = 0) const; + int entryIndex(const ArchiveEntry* entry, size_t startfrom = 0) const; vector> allEntries() const; vector> allDirectories() const; diff --git a/src/Archive/ArchiveEntry.cpp b/src/Archive/ArchiveEntry.cpp index c03124dc6..42af26b2e 100644 --- a/src/Archive/ArchiveEntry.cpp +++ b/src/Archive/ArchiveEntry.cpp @@ -96,6 +96,7 @@ ArchiveEntry::ArchiveEntry(const ArchiveEntry& copy) : data_{ copy.data_ }, type_{ copy.type_ }, ex_props_{ copy.ex_props_ }, + data_hash_{ copy.data_hash_ }, encrypted_{ copy.encrypted_ }, reliability_{ copy.reliability_ } { @@ -166,7 +167,7 @@ string ArchiveEntry::path(bool include_name) const // Returns the 'next' entry from this (ie. index + 1) in its parent ArchiveDir, // or nullptr if it is the last entry or has no parent dir // ----------------------------------------------------------------------------- -ArchiveEntry* ArchiveEntry::nextEntry() +ArchiveEntry* ArchiveEntry::nextEntry() const { return parent_ ? parent_->entryAt(parent_->entryIndex(this) + 1) : nullptr; } @@ -175,7 +176,7 @@ ArchiveEntry* ArchiveEntry::nextEntry() // Returns the 'previous' entry from this (ie. index - 1) in its parent // ArchiveDir, or nullptr if it is the first entry or has no parent dir // ----------------------------------------------------------------------------- -ArchiveEntry* ArchiveEntry::prevEntry() +ArchiveEntry* ArchiveEntry::prevEntry() const { return parent_ ? parent_->entryAt(parent_->entryIndex(this) - 1) : nullptr; } @@ -193,11 +194,22 @@ shared_ptr ArchiveEntry::getShared() const // Returns the entry's index within its parent ArchiveDir, or -1 if it isn't // in a dir // ----------------------------------------------------------------------------- -int ArchiveEntry::index() +int ArchiveEntry::index() const { return parent_ ? parent_->entryIndex(this) : -1; } +// ----------------------------------------------------------------------------- +// Returns the hash (XXH128) of the entry's data, calculating it if needed +// ----------------------------------------------------------------------------- +const string& ArchiveEntry::hash() const +{ + if (data_hash_.empty()) + data_hash_ = data_.hash(); + + return data_hash_; +} + // ----------------------------------------------------------------------------- // Sets the entry's name (but doesn't change state to modified) // ----------------------------------------------------------------------------- @@ -321,7 +333,14 @@ bool ArchiveEntry::resize(uint32_t new_size, bool preserve_data) // Update attributes setState(EntryState::Modified); - return data_.reSize(new_size, preserve_data); + // Resize the data + if (data_.reSize(new_size, preserve_data)) + { + data_hash_ = data_.hash(); + return true; + } + + return false; } // ----------------------------------------------------------------------------- @@ -338,6 +357,7 @@ bool ArchiveEntry::clearData() // Delete the data data_.clear(); + data_hash_.clear(); return true; } @@ -374,6 +394,7 @@ bool ArchiveEntry::importMem(const void* data, uint32_t size) data_.importMem(static_cast(data), size); // Update attributes + data_hash_ = data_.hash(); setType(EntryType::unknownType()); setState(EntryState::Modified); @@ -489,6 +510,7 @@ bool ArchiveEntry::importFileStream(wxFile& file, uint32_t len) if (data_.importFileStreamWx(file, len)) { // Update attributes + data_hash_ = data_.hash(); setType(EntryType::unknownType()); setState(EntryState::Modified); @@ -561,6 +583,7 @@ bool ArchiveEntry::write(const void* data, uint32_t size) if (data_.write(data, size)) { // Update attributes + data_hash_.clear(); setState(EntryState::Modified); return true; @@ -657,17 +680,17 @@ bool ArchiveEntry::isInNamespace(string_view ns) // Returns the entry at [path] relative to [base], or failing that, the entry // at absolute [path] in the archive (if [allow_absolute_path] is true) // ----------------------------------------------------------------------------- -ArchiveEntry* ArchiveEntry::relativeEntry(string_view at_path, bool allow_absolute_path) const +ArchiveEntry* ArchiveEntry::relativeEntry(string_view path, bool allow_absolute_path) const { if (!parent_) return nullptr; // Try relative to this entry - auto include = parent_->archive()->entryAtPath(path().append(at_path)); + auto include = parent_->archive()->entryAtPath(this->path().append(path)); // Try absolute path if (!include && allow_absolute_path) - include = parent_->archive()->entryAtPath(at_path); + include = parent_->archive()->entryAtPath(path); return include; } diff --git a/src/Archive/ArchiveEntry.h b/src/Archive/ArchiveEntry.h index d0206954a..40f8a2f2c 100644 --- a/src/Archive/ArchiveEntry.h +++ b/src/Archive/ArchiveEntry.h @@ -49,10 +49,12 @@ class ArchiveEntry EntryState state() const { return state_; } bool isLocked() const { return locked_; } EntryEncryption encryption() const { return encrypted_; } - ArchiveEntry* nextEntry(); - ArchiveEntry* prevEntry(); + ArchiveEntry* nextEntry() const; + ArchiveEntry* prevEntry() const; shared_ptr getShared() const; - int index(); + int index() const; + const string& hash() const; + int64_t libraryId() const { return library_id_; } // Modifiers (won't change entry state, except setState of course :P) void setName(string_view name); @@ -63,6 +65,7 @@ class ArchiveEntry } void setState(EntryState state, bool silent = false); void setEncryption(EntryEncryption enc) { encrypted_ = enc; } + void setLibraryId(int64_t id) const { library_id_ = id; } void lock(); void unlock(); void lockState() { state_locked_ = true; } @@ -111,12 +114,13 @@ class ArchiveEntry private: // Entry Info - string name_; - string upper_name_; - MemChunk data_; - EntryType* type_ = nullptr; - ArchiveDir* parent_ = nullptr; - PropertyList ex_props_; + string name_; + string upper_name_; + MemChunk data_; + EntryType* type_ = nullptr; + ArchiveDir* parent_ = nullptr; + PropertyList ex_props_; + mutable string data_hash_; // Entry status EntryState state_ = EntryState::New; @@ -125,8 +129,9 @@ class ArchiveEntry EntryEncryption encrypted_ = EntryEncryption::None; // Is there some encrypting on the archive? // Misc stuff - int reliability_ = 0; // The reliability of the entry's identification - size_t index_guess_ = 0; // for speed + int reliability_ = 0; // The reliability of the entry's identification + mutable size_t index_guess_ = 0; // for speed + mutable int64_t library_id_ = -1; // The id of this entry in the library }; template T ArchiveEntry::exProp(const string& key) diff --git a/src/Archive/ArchiveFormat.cpp b/src/Archive/ArchiveFormat.cpp index d0c696a76..59a3ed283 100644 --- a/src/Archive/ArchiveFormat.cpp +++ b/src/Archive/ArchiveFormat.cpp @@ -34,6 +34,7 @@ #include "ArchiveFormat.h" #include "Utility/Named.h" #include "Utility/Parser.h" +#include "Utility/PropertyList.h" using namespace slade; using namespace archive; @@ -227,3 +228,16 @@ ArchiveFormat archive::formatFromId(string_view format_id_string) return ArchiveFormat::Unknown; } + +// ----------------------------------------------------------------------------- +// Returns true if [file_ext] is a known archive file extension +// ----------------------------------------------------------------------------- +bool archive::isKnownExtension(string_view file_ext) +{ + for (auto& [format, info] : format_info) + for (const auto& ext : info.extensions) + if (strutil::equalCI(ext.first, file_ext)) + return true; + + return false; +} diff --git a/src/Archive/ArchiveFormat.h b/src/Archive/ArchiveFormat.h index 821243083..31372b802 100644 --- a/src/Archive/ArchiveFormat.h +++ b/src/Archive/ArchiveFormat.h @@ -59,5 +59,6 @@ namespace archive const ArchiveFormatInfo& formatInfoFromId(string_view id); string formatId(ArchiveFormat format); ArchiveFormat formatFromId(string_view format_id_string); + bool isKnownExtension(string_view file_ext); } // namespace archive } // namespace slade diff --git a/src/Archive/ArchiveFormatHandler.cpp b/src/Archive/ArchiveFormatHandler.cpp index a1c979bb2..931851f02 100644 --- a/src/Archive/ArchiveFormatHandler.cpp +++ b/src/Archive/ArchiveFormatHandler.cpp @@ -66,7 +66,7 @@ vector> all_handlers; class EntryRenameUS : public UndoStep { public: - EntryRenameUS(ArchiveEntry* entry, string_view new_name) : + EntryRenameUS(const ArchiveEntry* entry, string_view new_name) : archive_{ entry->parent() }, entry_path_{ entry->path() }, entry_index_{ entry->index() }, @@ -183,7 +183,7 @@ class EntrySwapUS : public UndoStep class EntryCreateDeleteUS : public UndoStep { public: - EntryCreateDeleteUS(bool created, ArchiveEntry* entry) : + EntryCreateDeleteUS(bool created, const ArchiveEntry* entry) : created_{ created }, archive_{ entry->parent() }, entry_copy_{ new ArchiveEntry(*entry) }, @@ -326,7 +326,7 @@ ArchiveFormatHandler::ArchiveFormatHandler(ArchiveFormat format, bool treeless) // Reads an archive from disk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ArchiveFormatHandler::open(Archive& archive, string_view filename) +bool ArchiveFormatHandler::open(Archive& archive, string_view filename, bool detect_types) { // Read the file into a MemChunk MemChunk mc; @@ -337,18 +337,18 @@ bool ArchiveFormatHandler::open(Archive& archive, string_view filename) } // Load from MemChunk - return open(archive, mc); + return open(archive, mc, detect_types); } // ----------------------------------------------------------------------------- // Reads an archive from an ArchiveEntry // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ArchiveFormatHandler::open(Archive& archive, ArchiveEntry* entry) +bool ArchiveFormatHandler::open(Archive& archive, ArchiveEntry* entry, bool detect_types) { // Load from entry's data auto sp_entry = entry->getShared(); - if (sp_entry && open(archive, sp_entry->data())) + if (sp_entry && open(archive, sp_entry->data(), detect_types)) { // Update variables and return success archive.parent_ = sp_entry; @@ -362,7 +362,7 @@ bool ArchiveFormatHandler::open(Archive& archive, ArchiveEntry* entry) // Reads an archive from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ArchiveFormatHandler::open(Archive& archive, const MemChunk& mc) +bool ArchiveFormatHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Invalid return false; @@ -808,7 +808,7 @@ bool ArchiveFormatHandler::renameEntry(Archive& archive, ArchiveEntry* entry, st // Returns the MapDesc information about the map beginning at [maphead]. // To be implemented in Archive sub-classes. // ----------------------------------------------------------------------------- -MapDesc ArchiveFormatHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) +MapDesc ArchiveFormatHandler::mapDesc(const Archive& archive, ArchiveEntry* maphead) { return {}; } @@ -817,7 +817,7 @@ MapDesc ArchiveFormatHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) // Returns the MapDesc information about all maps in the Archive. // To be implemented in Archive sub-classes. // ----------------------------------------------------------------------------- -vector ArchiveFormatHandler::detectMaps(Archive& archive) +vector ArchiveFormatHandler::detectMaps(const Archive& archive) { return {}; } @@ -825,7 +825,7 @@ vector ArchiveFormatHandler::detectMaps(Archive& archive) // ----------------------------------------------------------------------------- // Returns the namespace of the entry at [index] within [dir] // ----------------------------------------------------------------------------- -string ArchiveFormatHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir) +string ArchiveFormatHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir) { if (dir && index < dir->numEntries()) return detectNamespace(archive, dir->entryAt(index)); @@ -836,7 +836,7 @@ string ArchiveFormatHandler::detectNamespace(Archive& archive, unsigned index, A // ----------------------------------------------------------------------------- // Returns the namespace that [entry] is within // ----------------------------------------------------------------------------- -string ArchiveFormatHandler::detectNamespace(Archive& archive, ArchiveEntry* entry) +string ArchiveFormatHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry) { // Check entry if (!archive.checkEntry(entry)) @@ -862,7 +862,7 @@ string ArchiveFormatHandler::detectNamespace(Archive& archive, ArchiveEntry* ent // Returns the first entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* ArchiveFormatHandler::findFirst(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* ArchiveFormatHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = options.dir; @@ -931,7 +931,7 @@ ArchiveEntry* ArchiveFormatHandler::findFirst(Archive& archive, ArchiveSearchOpt // Returns the last entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* ArchiveFormatHandler::findLast(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* ArchiveFormatHandler::findLast(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = options.dir; @@ -999,7 +999,7 @@ ArchiveEntry* ArchiveFormatHandler::findLast(Archive& archive, ArchiveSearchOpti // ----------------------------------------------------------------------------- // Returns a list of entries matching the search criteria in [options] // ----------------------------------------------------------------------------- -vector ArchiveFormatHandler::findAll(Archive& archive, ArchiveSearchOptions& options) +vector ArchiveFormatHandler::findAll(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = options.dir; @@ -1065,16 +1065,6 @@ vector ArchiveFormatHandler::findAll(Archive& archive, ArchiveSea return ret; } -// ----------------------------------------------------------------------------- -// Detects type for all entries in [archive] -// (just here because it's a protected function in Archive, so subclasses can't -// access it directly) -// ----------------------------------------------------------------------------- -void ArchiveFormatHandler::detectAllEntryTypes(const Archive& archive) -{ - archive.detectAllEntryTypes(); -} - // ----------------------------------------------------------------------------- // Returns true if data in [mc] is a file // ----------------------------------------------------------------------------- diff --git a/src/Archive/ArchiveFormatHandler.h b/src/Archive/ArchiveFormatHandler.h index c8b4f6c79..579704056 100644 --- a/src/Archive/ArchiveFormatHandler.h +++ b/src/Archive/ArchiveFormatHandler.h @@ -22,9 +22,9 @@ class ArchiveFormatHandler ArchiveFormat format() const { return format_; } // Opening - virtual bool open(Archive& archive, string_view filename); // Open from File - virtual bool open(Archive& archive, ArchiveEntry* entry); // Open from ArchiveEntry - virtual bool open(Archive& archive, const MemChunk& mc); // Open from MemChunk + virtual bool open(Archive& archive, string_view filename, bool detect_types); // Open from File + virtual bool open(Archive& archive, ArchiveEntry* entry, bool detect_types); // Open from ArchiveEntry + virtual bool open(Archive& archive, const MemChunk& mc, bool detect_types); // Open from MemChunk // Writing/Saving virtual bool write(Archive& archive, MemChunk& mc); // Write to MemChunk @@ -68,15 +68,15 @@ class ArchiveFormatHandler virtual bool renameEntry(Archive& archive, ArchiveEntry* entry, string_view name, bool force = false); // Detection - virtual MapDesc mapDesc(Archive& archive, ArchiveEntry* maphead); - virtual vector detectMaps(Archive& archive); - virtual string detectNamespace(Archive& archive, ArchiveEntry* entry); - virtual string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr); + virtual MapDesc mapDesc(const Archive& archive, ArchiveEntry* maphead); + virtual vector detectMaps(const Archive& archive); + virtual string detectNamespace(const Archive& archive, ArchiveEntry* entry); + virtual string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr); // Search - virtual ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options); - virtual ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options); - virtual vector findAll(Archive& archive, ArchiveSearchOptions& options); + virtual ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options); + virtual ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options); + virtual vector findAll(const Archive& archive, ArchiveSearchOptions& options); // Format detection virtual bool isThisFormat(const MemChunk& mc); @@ -85,9 +85,6 @@ class ArchiveFormatHandler protected: ArchiveFormat format_; bool treeless_ = false; - - // Temp shortcuts - void detectAllEntryTypes(const Archive& archive); }; namespace archive diff --git a/src/Archive/ArchiveManager.cpp b/src/Archive/ArchiveManager.cpp index edfc60611..4ab4356f1 100644 --- a/src/Archive/ArchiveManager.cpp +++ b/src/Archive/ArchiveManager.cpp @@ -41,6 +41,8 @@ #include "General/Console.h" #include "General/ResourceManager.h" #include "General/UI.h" +#include "Library/Library.h" +#include "Utility/DateTime.h" #include "Utility/FileUtils.h" #include "Utility/StringUtils.h" @@ -57,6 +59,67 @@ CVAR(Int, max_recent_files, 25, CVar::Flag::Save) CVAR(Bool, auto_open_wads_root, false, CVar::Flag::Save) +// ----------------------------------------------------------------------------- +// +// Functions +// +// ----------------------------------------------------------------------------- +namespace +{ +// ----------------------------------------------------------------------------- +// Updates/Adds [archive] in/to the library and updates last opened time if +// requested. +// Returns true if the archive was added to the library (and thus entry types +// were already detected) +// ----------------------------------------------------------------------------- +bool updateArchiveInLibrary(const Archive& archive, bool update_last_opened) +{ + ui::setSplashProgressMessage("Updating Library"); + ui::setSplashProgress(1.0f); + + // Read info from library into archive + auto lib_id = library::readArchiveInfo(archive); + + // If it wasn't in the library, add it + auto added = false; + if (lib_id < 0) + { + // Need to detect all entry types before adding + archive.detectAllEntryTypes(false); + + // Add to library + ui::setSplashProgressMessage("Updating Library"); + lib_id = library::writeArchiveInfo(archive); + + added = true; + } + + // Update last opened time if needed + if (update_last_opened) + library::setArchiveLastOpenedTime(lib_id, datetime::now()); + + ui::setSplashProgressMessage(""); + + return added; +} + +// ----------------------------------------------------------------------------- +// Writes all bookmarked entries for [archive] to the library +// ----------------------------------------------------------------------------- +void writeArchiveBookmarksToLibrary(const Archive* archive, const vector>& bookmarks) +{ + // Remove existing bookmarks for [archive] in library + library::removeArchiveBookmarks(archive->libraryId()); + + // Add any bookmarks that are in [archive] to the library + for (auto& bookmark : bookmarks) + if (auto entry = bookmark.lock().get()) + if (entry->parent() == archive) + library::addBookmark(archive->libraryId(), entry->libraryId()); +} +} // namespace + + // ----------------------------------------------------------------------------- // // ArchiveManager Class Functions @@ -118,7 +181,8 @@ bool ArchiveManager::init() program_resource_archive_ = std::make_unique(ArchiveFormat::Zip); #ifdef __WXOSX__ - auto resdir = app::path("../Resources", app::Dir::Executable); // Use Resources dir within bundle on mac + // Use Resources dir within bundle on mac + auto resdir = app::path("../Resources", app::Dir::Executable); #else auto resdir = app::path("res", app::Dir::Executable); #endif @@ -146,7 +210,7 @@ bool ArchiveManager::init() dir_slade_pk3 = "slade.pk3"; // Open slade.pk3 - if (!program_resource_archive_->open(dir_slade_pk3)) + if (!program_resource_archive_->open(dir_slade_pk3, true)) { log::error("Unable to find slade.pk3!"); res_archive_open_ = false; @@ -173,7 +237,7 @@ bool ArchiveManager::initArchiveFormats() const // ----------------------------------------------------------------------------- bool ArchiveManager::initBaseResource() { - return openBaseResource((int)base_resource); + return openBaseResource(base_resource); } // ----------------------------------------------------------------------------- @@ -193,12 +257,27 @@ bool ArchiveManager::addArchive(shared_ptr archive) // Emit archive changed/saved signal when received from the archive archive->signals().modified.connect([this](Archive& archive, bool modified) { signals_.archive_modified(archiveIndex(&archive), modified); }); - archive->signals().saved.connect([this](Archive& archive) { signals_.archive_saved(archiveIndex(&archive)); }); + archive->signals().saved.connect( + [this](Archive& archive) + { + // Update in library + if (archive.isOnDisk()) + { + archive.setLibraryId(library::writeArchiveInfo(archive)); + + // (Re)Write bookmarks since entry ids have likely changed + // and/or been added + writeArchiveBookmarksToLibrary(&archive, bookmarks_); + } + + signals_.archive_saved(archiveIndex(&archive)); + }); // Announce the addition signals_.archive_added(open_archives_.size() - 1); // Add to resource manager + ui::setSplashProgressMessage("Loading Resources"); app::resources().addArchive(archive.get()); // ZDoom also loads any WADs found in the root of a PK3 or directory @@ -258,10 +337,10 @@ shared_ptr ArchiveManager::getArchive(string_view filename) // ---------------------------------------------------------------------------- shared_ptr ArchiveManager::getArchive(const ArchiveEntry* parent) { - for (unsigned a = 0; a < open_archives_.size(); ++a) + for (auto& open_archive : open_archives_) { - if (open_archives_[a].archive->parentEntry() == parent) - return open_archives_[a].archive; + if (open_archive.archive->parentEntry() == parent) + return open_archive.archive; } return nullptr; @@ -304,32 +383,36 @@ shared_ptr ArchiveManager::openArchive(string_view filename, bool manag // Create archive new_archive = std::make_shared(format_id); - // If it opened successfully, add it to the list if needed & return it, - // Otherwise, delete it and return nullptr - if (new_archive->open(filename)) + // Attempt to open archive + if (!new_archive->open(filename, false)) { - if (manage) - { - // Add the archive - auto index = open_archives_.size(); - addArchive(new_archive); + log::error(global::error); + return nullptr; + } - // Announce open - if (!silent) - signals_.archive_opened(index); + // Opened ok, add to manager if requested + if (manage) + { + // Add/update in library + auto added = updateArchiveInLibrary(*new_archive, true); - // Add to recent files - addRecentFile(filename); - } + // Detect entry types if needed (use hints from library) + if (!added) + new_archive->detectAllEntryTypes(); - // Return the opened archive - return new_archive; - } - else - { - log::error(global::error); - return nullptr; + // Restore bookmarks + addBookmarksFromLibrary(*new_archive); + + // Add the archive + auto index = open_archives_.size(); + addArchive(new_archive); + + // Announce open + if (!silent) + signals_.archive_opened(index); } + + return new_archive; } // ----------------------------------------------------------------------------- @@ -366,37 +449,42 @@ shared_ptr ArchiveManager::openArchive(ArchiveEntry* entry, bool manage // Create archive auto new_archive = std::make_shared(format_id); - // If it opened successfully, add it to the list & return it, - // Otherwise, delete it and return nullptr - if (new_archive->open(entry)) + // Attempt to open archive + if (!new_archive->open(entry, false)) { - if (manage) - { - // Add to parent's child list if parent is open in the manager (it should be) - int index_parent = -1; - if (entry->parent()) - index_parent = archiveIndex(entry->parent()); - if (index_parent >= 0) - open_archives_[index_parent].open_children.emplace_back(new_archive); + log::error(global::error); + return nullptr; + } - // Add the new archive - auto index = open_archives_.size(); - addArchive(new_archive); + // Opened ok, add to manager if requested + if (manage) + { + // Add/update in library + auto added = updateArchiveInLibrary(*new_archive, true); - // Announce open - if (!silent) - signals_.archive_opened(index); + // Detect entry types if needed (use hints from library) + if (!added) + new_archive->detectAllEntryTypes(); - entry->lock(); - } + // Add to parent's child list if parent is open in the manager (it should be) + int index_parent = -1; + if (entry->parent()) + index_parent = archiveIndex(entry->parent()); + if (index_parent >= 0) + open_archives_[index_parent].open_children.emplace_back(new_archive); - return new_archive; - } - else - { - log::error(global::error); - return nullptr; + // Add the archive + auto index = open_archives_.size(); + addArchive(new_archive); + + // Announce open + if (!silent) + signals_.archive_opened(index); + + entry->lock(); } + + return new_archive; } // ----------------------------------------------------------------------------- @@ -421,32 +509,36 @@ shared_ptr ArchiveManager::openDirArchive(string_view dir, bool manage, new_archive = std::make_shared(ArchiveFormat::Dir); - // If it opened successfully, add it to the list if needed & return it, - // Otherwise, delete it and return nullptr - if (new_archive->open(dir)) + // Attempt to open archive + if (!new_archive->open(dir, false)) { - if (manage) - { - // Add the archive - auto index = open_archives_.size(); - addArchive(new_archive); + log::error(global::error); + return nullptr; + } - // Announce open - if (!silent) - signals_.archive_opened(index); + // Opened ok, add to manager if requested + if (manage) + { + // Add/update in library + auto added = updateArchiveInLibrary(*new_archive, true); - // Add to recent files - addRecentFile(dir); - } + // Detect entry types if needed (use hints from library) + if (!added) + new_archive->detectAllEntryTypes(); - // Return the opened archive - return new_archive; - } - else - { - log::error(global::error); - return nullptr; + // Restore bookmarks + addBookmarksFromLibrary(*new_archive); + + // Add the archive + auto index = open_archives_.size(); + addArchive(new_archive); + + // Announce open + if (!silent) + signals_.archive_opened(index); } + + return new_archive; } // ----------------------------------------------------------------------------- @@ -494,13 +586,14 @@ bool ArchiveManager::closeArchive(int index) signals_.archive_closing(index); // Delete any bookmarked entries contained in the archive - deleteBookmarksInArchive(open_archives_[index].archive.get()); + deleteBookmarksInArchive(open_archives_[index].archive.get(), false); // Remove from resource manager app::resources().removeArchive(open_archives_[index].archive.get()); // Close any open child archives - // Clear out the open_children vector first, lest the children try to remove themselves from it + // Clear out the open_children vector first, lest the children try to remove + // themselves from it auto open_children = open_archives_[index].open_children; open_archives_[index].open_children.clear(); for (auto& archive : open_children) @@ -533,6 +626,14 @@ bool ArchiveManager::closeArchive(int index) parent->unlock(); } + // Update library + if (open_archives_[index].archive->isOnDisk()) + { + const auto& archive = *open_archives_[index].archive; + library::writeArchiveEntryInfo(archive); + library::writeArchiveMapInfo(archive); + } + // Close the archive open_archives_[index].archive->close(); @@ -789,6 +890,14 @@ string ArchiveManager::getBaseResourcePath(unsigned index) return base_resource_paths_[index]; } +// ----------------------------------------------------------------------------- +// Returns the currently selected base resource path +// ----------------------------------------------------------------------------- +string ArchiveManager::currentBaseResourcePath() +{ + return getBaseResourcePath(base_resource); +} + // ----------------------------------------------------------------------------- // Opens the base resource archive [index] // ----------------------------------------------------------------------------- @@ -824,10 +933,11 @@ bool ArchiveManager::openBaseResource(int index) // Attempt to open the file ui::showSplash(fmt::format("Opening {}...", filename), true); - if (base_resource_archive_->open(filename)) + if (base_resource_archive_->open(filename, true)) { base_resource = index; ui::hideSplash(); + updateArchiveInLibrary(*base_resource_archive_, false); app::resources().addArchive(base_resource_archive_.get()); signals_.base_res_current_changed(index); return true; @@ -928,94 +1038,6 @@ vector ArchiveManager::findAllResourceEntries(ArchiveSearchOption return ret; } -// ----------------------------------------------------------------------------- -// Returns the recent file path at [index] -// ----------------------------------------------------------------------------- -string ArchiveManager::recentFile(unsigned index) -{ - // Check index - if (index >= recent_files_.size()) - return ""; - - return recent_files_[index]; -} - -// ----------------------------------------------------------------------------- -// Adds a recent file to the list, if it doesn't exist already -// ----------------------------------------------------------------------------- -void ArchiveManager::addRecentFile(string_view path) -{ - // Check the path is valid - if (!(fileutil::fileExists(path) || fileutil::dirExists(path))) - return; - - // Replace \ with / - auto file_path = string{ path }; - std::replace(file_path.begin(), file_path.end(), '\\', '/'); - - // Check if the file is already in the list - for (unsigned a = 0; a < recent_files_.size(); a++) - { - if (recent_files_[a] == file_path) - { - // Move this file to the top of the list - recent_files_.erase(recent_files_.begin() + a); - recent_files_.insert(recent_files_.begin(), file_path); - - // Announce - signals_.recent_files_changed(); - - return; - } - } - - // Add the file to the top of the list - recent_files_.insert(recent_files_.begin(), file_path); - - // Keep it trimmed - while (recent_files_.size() > static_cast(max_recent_files)) - recent_files_.pop_back(); - - // Announce - signals_.recent_files_changed(); -} - -// ----------------------------------------------------------------------------- -// Adds a list of recent file paths to the recent file list -// ----------------------------------------------------------------------------- -void ArchiveManager::addRecentFiles(const vector& paths) -{ - // Mute annoucements - signals_.recent_files_changed.block(); - - // Clear existing list - recent_files_.clear(); - - // Add the files - for (const auto& path : paths) - addRecentFile(path); - - // Announce - signals_.recent_files_changed.unblock(); - signals_.recent_files_changed(); -} - -// ----------------------------------------------------------------------------- -// Removes the recent file matching [path] -// ----------------------------------------------------------------------------- -void ArchiveManager::removeRecentFile(string_view path) -{ - for (unsigned a = 0; a < recent_files_.size(); a++) - { - if (recent_files_[a] == path) - { - recent_files_.erase(recent_files_.begin() + a); - signals_.recent_files_changed(); - return; - } - } -} - // ----------------------------------------------------------------------------- // Adds [entry] to the bookmark list // ----------------------------------------------------------------------------- @@ -1033,6 +1055,7 @@ void ArchiveManager::addBookmark(const shared_ptr& entry) // Add bookmark bookmarks_.push_back(entry); + library::addBookmark(entry->parent()->libraryId(), entry->libraryId()); // Announce signals_.bookmark_added(entry.get()); @@ -1049,10 +1072,10 @@ bool ArchiveManager::deleteBookmark(ArchiveEntry* entry) if (bookmarks_[a].lock().get() == entry) { // Remove it - bookmarks_.erase(bookmarks_.begin() + a); + removeBookmark(a); // Announce - signals_.bookmarks_removed(vector(1, entry)); + signals_.bookmarks_removed({ 1, entry }); return true; } @@ -1073,10 +1096,10 @@ bool ArchiveManager::deleteBookmark(unsigned index) // Remove bookmark auto* entry = bookmarks_[index].lock().get(); - bookmarks_.erase(bookmarks_.begin() + index); + removeBookmark(index); // Announce - signals_.bookmarks_removed(vector(1, entry)); + signals_.bookmarks_removed({ 1, entry }); return true; } @@ -1084,7 +1107,7 @@ bool ArchiveManager::deleteBookmark(unsigned index) // ----------------------------------------------------------------------------- // Removes any bookmarked entries in [archive] from the list // ----------------------------------------------------------------------------- -bool ArchiveManager::deleteBookmarksInArchive(const Archive* archive) +bool ArchiveManager::deleteBookmarksInArchive(const Archive* archive, bool remove_from_library) { // Go through bookmarks bool removed = false; @@ -1096,7 +1119,10 @@ bool ArchiveManager::deleteBookmarksInArchive(const Archive* archive) if (!bookmark || bookmark->parent() == archive) { removed_entries.push_back(bookmark.get()); - bookmarks_.erase(bookmarks_.begin() + a); + if (remove_from_library) + removeBookmark(a); + else + bookmarks_.erase(bookmarks_.begin() + a); a--; removed = true; } @@ -1151,7 +1177,7 @@ bool ArchiveManager::deleteBookmarksInDir(const ArchiveDir* node) if (remove) { removed_entries.push_back(bookmark.get()); - bookmarks_.erase(bookmarks_.begin() + a); + removeBookmark(a); --a; removed = true; } @@ -1177,7 +1203,11 @@ void ArchiveManager::deleteAllBookmarks() { vector removed; for (const auto& entry : bookmarks_) - removed.push_back(entry.lock().get()); + { + auto sp = entry.lock(); + removed.push_back(sp.get()); + library::removeBookmark(sp->parent()->libraryId(), sp->libraryId()); + } bookmarks_.clear(); signals_.bookmarks_removed(removed); @@ -1208,6 +1238,34 @@ bool slade::ArchiveManager::isBookmarked(const ArchiveEntry* entry) const return false; } +// ----------------------------------------------------------------------------- +// Helper function that removes the bookmark at [index] and handles library +// updates etc. +// ----------------------------------------------------------------------------- +void ArchiveManager::removeBookmark(unsigned index) +{ + auto* entry = bookmarks_[index].lock().get(); + + bookmarks_.erase(bookmarks_.begin() + index); + + if (entry) + library::removeBookmark(entry->parent()->libraryId(), entry->libraryId()); +} + +// ----------------------------------------------------------------------------- +// Adds all bookmarks from the library for [archive] +// ----------------------------------------------------------------------------- +void ArchiveManager::addBookmarksFromLibrary(const Archive& archive) +{ + auto bookmark_ids = library::bookmarkedEntries(archive.libraryId()); + + vector entries; + archive.putEntryTreeAsList(entries); + for (auto entry : entries) + if (VECTOR_EXISTS(bookmark_ids, entry->libraryId())) + addBookmark(entry->getShared()); +} + // ----------------------------------------------------------------------------- // diff --git a/src/Archive/ArchiveManager.h b/src/Archive/ArchiveManager.h index 7132f6040..dfe360dce 100644 --- a/src/Archive/ArchiveManager.h +++ b/src/Archive/ArchiveManager.h @@ -39,7 +39,6 @@ class ArchiveManager shared_ptr shareArchive(const Archive* archive); // General access - const vector& recentFiles() const { return recent_files_; } const vector& baseResourcePaths() const { return base_resource_paths_; } const vector>& bookmarks() const { return bookmarks_; } @@ -49,6 +48,7 @@ class ArchiveManager void removeBaseResourcePath(unsigned index); unsigned numBaseResourcePaths() const { return base_resource_paths_.size(); } string getBaseResourcePath(unsigned index); + string currentBaseResourcePath(); bool openBaseResource(int index); // Resource entry get/search @@ -56,18 +56,11 @@ class ArchiveManager ArchiveEntry* findResourceEntry(ArchiveSearchOptions& options, const Archive* ignore = nullptr) const; vector findAllResourceEntries(ArchiveSearchOptions& options, const Archive* ignore = nullptr) const; - // Recent files - string recentFile(unsigned index); - unsigned numRecentFiles() const { return recent_files_.size(); } - void addRecentFile(string_view path); - void addRecentFiles(const vector& paths); - void removeRecentFile(string_view path); - // Bookmarks void addBookmark(const shared_ptr& entry); bool deleteBookmark(ArchiveEntry* entry); bool deleteBookmark(unsigned index); - bool deleteBookmarksInArchive(const Archive* archive); + bool deleteBookmarksInArchive(const Archive* archive, bool remove_from_library = true); bool deleteBookmarksInDir(const ArchiveDir* node); void deleteAllBookmarks(); ArchiveEntry* getBookmark(unsigned index) const; @@ -87,7 +80,6 @@ class ArchiveManager sigslot::signal base_res_path_removed; sigslot::signal base_res_current_changed; sigslot::signal<> base_res_current_cleared; - sigslot::signal<> recent_files_changed; sigslot::signal bookmark_added; sigslot::signal&> bookmarks_removed; }; @@ -98,7 +90,7 @@ class ArchiveManager { shared_ptr archive; vector> open_children; // A list of currently open archives that are within this archive - bool resource; + bool resource = true; }; vector open_archives_; @@ -106,13 +98,14 @@ class ArchiveManager shared_ptr base_resource_archive_; bool res_archive_open_ = false; vector base_resource_paths_; - vector recent_files_; vector> bookmarks_; // Signals Signals signals_; - bool initArchiveFormats() const; - void getDependentArchivesInternal(const Archive* archive, vector>& vec); + bool initArchiveFormats() const; + void getDependentArchivesInternal(const Archive* archive, vector>& vec); + inline void removeBookmark(unsigned index); + void addBookmarksFromLibrary(const Archive& archive); }; } // namespace slade diff --git a/src/Archive/EntryType/EntryType.cpp b/src/Archive/EntryType/EntryType.cpp index 1f1bd6144..b30f60e3f 100644 --- a/src/Archive/EntryType/EntryType.cpp +++ b/src/Archive/EntryType/EntryType.cpp @@ -622,6 +622,20 @@ bool EntryType::detectEntryType(ArchiveEntry& entry) // Reset entry type entry.setType(etype_unknown); + // Check for hinted type first + if (entry.exProps().contains("TypeHint")) + { + auto hint_type = fromId(std::get(entry.exProp("TypeHint"))); + if (auto r = hint_type->isThisType(entry); r > 0) + { + entry.setType(hint_type, r); + return true; + } + + // Wasn't the hinted type, remove hint + entry.exProps().remove("TypeHint"); + } + // Go through all registered types const size_t entry_types_size = entry_types.size(); for (size_t a = 0; a < entry_types_size; a++) @@ -676,6 +690,14 @@ EntryType* EntryType::folderType() return etype_folder; } +// ----------------------------------------------------------------------------- +// Returns the global 'marker' entry type +// ----------------------------------------------------------------------------- +EntryType* EntryType::markerType() +{ + return etype_marker; +} + // ----------------------------------------------------------------------------- // Returns the global 'map marker' entry type // ----------------------------------------------------------------------------- diff --git a/src/Archive/EntryType/EntryType.h b/src/Archive/EntryType/EntryType.h index 064dbcabc..79889bed9 100644 --- a/src/Archive/EntryType/EntryType.h +++ b/src/Archive/EntryType/EntryType.h @@ -41,6 +41,7 @@ class EntryType static EntryType* fromId(string_view id); static EntryType* unknownType(); static EntryType* folderType(); + static EntryType* markerType(); static EntryType* mapMarkerType(); static vector iconList(); static vector allTypes(); diff --git a/src/Archive/Formats/ADatArchiveHandler.cpp b/src/Archive/Formats/ADatArchiveHandler.cpp index a587798ba..fb76c88ea 100644 --- a/src/Archive/Formats/ADatArchiveHandler.cpp +++ b/src/Archive/Formats/ADatArchiveHandler.cpp @@ -52,7 +52,7 @@ using namespace slade; // Reads dat format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ADatArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool ADatArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check given data is valid if (mc.size() < 16) @@ -144,7 +144,8 @@ bool ADatArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/ADatArchiveHandler.h b/src/Archive/Formats/ADatArchiveHandler.h index 50dc8a829..b9ed93abe 100644 --- a/src/Archive/Formats/ADatArchiveHandler.h +++ b/src/Archive/Formats/ADatArchiveHandler.h @@ -11,7 +11,7 @@ class ADatArchiveHandler : public ArchiveFormatHandler ~ADatArchiveHandler() override = default; // Opening - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk diff --git a/src/Archive/Formats/BSPArchiveHandler.cpp b/src/Archive/Formats/BSPArchiveHandler.cpp index c1307df99..066533f98 100644 --- a/src/Archive/Formats/BSPArchiveHandler.cpp +++ b/src/Archive/Formats/BSPArchiveHandler.cpp @@ -57,7 +57,7 @@ using namespace slade; // Reads BSP format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool BSPArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool BSPArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // If size is less than 64, there's not even enough room for a full header size_t size = mc.size(); @@ -218,7 +218,8 @@ bool BSPArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/BSPArchiveHandler.h b/src/Archive/Formats/BSPArchiveHandler.h index cddf3dba2..f2c2a8fd7 100644 --- a/src/Archive/Formats/BSPArchiveHandler.h +++ b/src/Archive/Formats/BSPArchiveHandler.h @@ -11,8 +11,8 @@ class BSPArchiveHandler : public ArchiveFormatHandler ~BSPArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/BZip2ArchiveHandler.cpp b/src/Archive/Formats/BZip2ArchiveHandler.cpp index eb01cc1d4..f1f912ff1 100644 --- a/src/Archive/Formats/BZip2ArchiveHandler.cpp +++ b/src/Archive/Formats/BZip2ArchiveHandler.cpp @@ -52,7 +52,7 @@ using namespace slade; // Reads bzip2 format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool BZip2ArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool BZip2ArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { size_t size = mc.size(); if (size < 14) @@ -151,7 +151,7 @@ bool BZip2ArchiveHandler::loadEntryData(Archive& archive, const ArchiveEntry* en // Returns the entry if it matches the search criteria in [options], // or null otherwise // ----------------------------------------------------------------------------- -ArchiveEntry* BZip2ArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* BZip2ArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables strutil::upperIP(options.match_name); @@ -191,7 +191,7 @@ ArchiveEntry* BZip2ArchiveHandler::findFirst(Archive& archive, ArchiveSearchOpti // ----------------------------------------------------------------------------- // Same as findFirst since there's just one entry // ----------------------------------------------------------------------------- -ArchiveEntry* BZip2ArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* BZip2ArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options) { return findFirst(archive, options); } @@ -199,7 +199,7 @@ ArchiveEntry* BZip2ArchiveHandler::findLast(Archive& archive, ArchiveSearchOptio // ----------------------------------------------------------------------------- // Returns all entries matching the search criteria in [options] // ----------------------------------------------------------------------------- -vector BZip2ArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options) +vector BZip2ArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables vector ret; diff --git a/src/Archive/Formats/BZip2ArchiveHandler.h b/src/Archive/Formats/BZip2ArchiveHandler.h index a68280244..0bd87e710 100644 --- a/src/Archive/Formats/BZip2ArchiveHandler.h +++ b/src/Archive/Formats/BZip2ArchiveHandler.h @@ -11,7 +11,7 @@ class BZip2ArchiveHandler : public ArchiveFormatHandler ~BZip2ArchiveHandler() override = default; // Opening - bool open(Archive& archive, const MemChunk& mc) override; + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; @@ -47,9 +47,9 @@ class BZip2ArchiveHandler : public ArchiveFormatHandler } // Search - ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override; - ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override; - vector findAll(Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override; + vector findAll(const Archive& archive, ArchiveSearchOptions& options) override; // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/ChasmBinArchiveHandler.cpp b/src/Archive/Formats/ChasmBinArchiveHandler.cpp index 14de0f3a3..45ad47f92 100644 --- a/src/Archive/Formats/ChasmBinArchiveHandler.cpp +++ b/src/Archive/Formats/ChasmBinArchiveHandler.cpp @@ -77,7 +77,7 @@ void fixBrokenWave(const ArchiveEntry* entry) // Reads Chasm bin format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ChasmBinArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool ChasmBinArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check given data is valid if (mc.size() < HEADER_SIZE) diff --git a/src/Archive/Formats/ChasmBinArchiveHandler.h b/src/Archive/Formats/ChasmBinArchiveHandler.h index c0160251d..0a0535ec3 100644 --- a/src/Archive/Formats/ChasmBinArchiveHandler.h +++ b/src/Archive/Formats/ChasmBinArchiveHandler.h @@ -10,7 +10,7 @@ class ChasmBinArchiveHandler : public ArchiveFormatHandler ChasmBinArchiveHandler() : ArchiveFormatHandler(ArchiveFormat::ChasmBin) {} // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; bool write(Archive& archive, MemChunk& mc) override; // Format detection diff --git a/src/Archive/Formats/DatArchiveHandler.cpp b/src/Archive/Formats/DatArchiveHandler.cpp index 9a6bef868..0f8a4e461 100644 --- a/src/Archive/Formats/DatArchiveHandler.cpp +++ b/src/Archive/Formats/DatArchiveHandler.cpp @@ -68,7 +68,7 @@ bool isNamespaceEntry(const ArchiveEntry* entry) // Reads wad format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -177,7 +177,8 @@ bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); @@ -191,7 +192,7 @@ bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc) // ----------------------------------------------------------------------------- // Returns the namespace that [entry] is within // ----------------------------------------------------------------------------- -string DatArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry) +string DatArchiveHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry) { return detectNamespace(archive, archive.entryIndex(entry)); } @@ -199,7 +200,7 @@ string DatArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry) // ----------------------------------------------------------------------------- // Returns the namespace that the entry at [index] in [dir] is within // ----------------------------------------------------------------------------- -string DatArchiveHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir) +string DatArchiveHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir) { // Textures if (index > static_cast(walls_[0]) && index < static_cast(walls_[1])) diff --git a/src/Archive/Formats/DatArchiveHandler.h b/src/Archive/Formats/DatArchiveHandler.h index b98cc32a1..9141cc926 100644 --- a/src/Archive/Formats/DatArchiveHandler.h +++ b/src/Archive/Formats/DatArchiveHandler.h @@ -14,8 +14,8 @@ class DatArchiveHandler : public ArchiveFormatHandler void updateNamespaces(Archive& archive); // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Entry addition/removal shared_ptr addEntry( @@ -36,8 +36,8 @@ class DatArchiveHandler : public ArchiveFormatHandler bool renameEntry(Archive& archive, ArchiveEntry* entry, string_view name, bool force = false) override; // Detection - string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override; - string detectNamespace(Archive& archive, ArchiveEntry* entry) override; + string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override; + string detectNamespace(const Archive& archive, ArchiveEntry* entry) override; // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/DirArchiveHandler.cpp b/src/Archive/Formats/DirArchiveHandler.cpp index 051714b71..291a737b5 100644 --- a/src/Archive/Formats/DirArchiveHandler.cpp +++ b/src/Archive/Formats/DirArchiveHandler.cpp @@ -88,7 +88,7 @@ DirArchiveHandler::DirArchiveHandler() : // Reads files from the directory [filename] into the archive // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool DirArchiveHandler::open(Archive& archive, string_view filename) +bool DirArchiveHandler::open(Archive& archive, string_view filename, bool detect_types) { ui::setSplashProgressMessage("Reading directory structure"); ui::setSplashProgress(0); @@ -131,9 +131,6 @@ bool DirArchiveHandler::open(Archive& archive, string_view filename) return false; file_modification_times_[new_entry.get()] = fileutil::fileModifiedTime(files[a]); - - // Detect entry type - EntryType::detectEntryType(*new_entry); } // Add empty directories @@ -154,6 +151,10 @@ bool DirArchiveHandler::open(Archive& archive, string_view filename) for (auto& entry : entry_list) entry->setState(EntryState::Unmodified); + // Detect all entry types + if (detect_types) + archive.detectAllEntryTypes(); + // Enable announcements sig_blocker.unblock(); @@ -167,7 +168,7 @@ bool DirArchiveHandler::open(Archive& archive, string_view filename) // ----------------------------------------------------------------------------- // Reads an archive from an ArchiveEntry (not implemented) // ----------------------------------------------------------------------------- -bool DirArchiveHandler::open(Archive& archive, ArchiveEntry* entry) +bool DirArchiveHandler::open(Archive& archive, ArchiveEntry* entry, bool detect_types) { global::error = "Cannot open Folder Archive from entry"; return false; @@ -176,7 +177,7 @@ bool DirArchiveHandler::open(Archive& archive, ArchiveEntry* entry) // ----------------------------------------------------------------------------- // Reads data from a MemChunk (not implemented) // ----------------------------------------------------------------------------- -bool DirArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool DirArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { global::error = "Cannot open Folder Archive from memory"; return false; @@ -450,7 +451,7 @@ bool DirArchiveHandler::renameEntry(Archive& archive, ArchiveEntry* entry, strin // Returns the mapdesc_t information about the map at [entry], if [entry] is // actually a valid map (ie. a wad archive in the maps folder) // ----------------------------------------------------------------------------- -MapDesc DirArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* entry) +MapDesc DirArchiveHandler::mapDesc(const Archive& archive, ArchiveEntry* entry) { MapDesc map; @@ -466,11 +467,20 @@ MapDesc DirArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* entry) if (entry->parentDir()->parent() != archive.rootDir() || entry->parentDir()->name() != "maps") return map; + // Detect map format + auto format = MapFormat::Unknown; + Archive tempwad(ArchiveFormat::Wad); + tempwad.open(entry->data(), true); + auto emaps = tempwad.detectMaps(); + if (!emaps.empty()) + format = emaps[0].format; + // Setup map info map.archive = true; map.head = entry->getShared(); map.end = entry->getShared(); map.name = entry->upperNameNoExt(); + map.format = format; return map; } @@ -479,7 +489,7 @@ MapDesc DirArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* entry) // Detects all the maps in the archive and returns a vector of information about // them. // ----------------------------------------------------------------------------- -vector DirArchiveHandler::detectMaps(Archive& archive) +vector DirArchiveHandler::detectMaps(const Archive& archive) { vector ret; @@ -500,7 +510,7 @@ vector DirArchiveHandler::detectMaps(Archive& archive) // Detect map format (probably kinda slow but whatever, no better way to do it really) auto format = MapFormat::Unknown; Archive tempwad(ArchiveFormat::Wad); - tempwad.open(entry->data()); + tempwad.open(entry->data(), true); auto emaps = tempwad.detectMaps(); if (!emaps.empty()) format = emaps[0].format; @@ -522,7 +532,7 @@ vector DirArchiveHandler::detectMaps(Archive& archive) // Returns the first entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* DirArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* DirArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = archive.rootDir().get(); @@ -555,7 +565,7 @@ ArchiveEntry* DirArchiveHandler::findFirst(Archive& archive, ArchiveSearchOption // Returns the last entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* DirArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* DirArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = archive.rootDir().get(); @@ -587,7 +597,7 @@ ArchiveEntry* DirArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions // ----------------------------------------------------------------------------- // Returns all entries matching the search criteria in [options] // ----------------------------------------------------------------------------- -vector DirArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options) +vector DirArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = archive.rootDir().get(); diff --git a/src/Archive/Formats/DirArchiveHandler.h b/src/Archive/Formats/DirArchiveHandler.h index aa0a96cc1..65f94453d 100644 --- a/src/Archive/Formats/DirArchiveHandler.h +++ b/src/Archive/Formats/DirArchiveHandler.h @@ -46,9 +46,9 @@ class DirArchiveHandler : public ArchiveFormatHandler bool saveErrorsOccurred() const { return save_errors_; } // Opening - bool open(Archive& archive, string_view filename) override; // Open from File - bool open(Archive& archive, ArchiveEntry* entry) override; // Open from ArchiveEntry - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk + bool open(Archive& archive, string_view filename, bool detect_types) override; // Open from File + bool open(Archive& archive, ArchiveEntry* entry, bool detect_types) override; // Open from ArchiveEntry + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk @@ -69,13 +69,13 @@ class DirArchiveHandler : public ArchiveFormatHandler bool renameEntry(Archive& archive, ArchiveEntry* entry, string_view name, bool force = false) override; // Detection - MapDesc mapDesc(Archive& archive, ArchiveEntry* entry) override; - vector detectMaps(Archive& archive) override; + MapDesc mapDesc(const Archive& archive, ArchiveEntry* entry) override; + vector detectMaps(const Archive& archive) override; // Search - ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override; - ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override; - vector findAll(Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override; + vector findAll(const Archive& archive, ArchiveSearchOptions& options) override; // DirArchiveHandler-specific void ignoreChangedEntries(const vector& changes); diff --git a/src/Archive/Formats/DiskArchiveHandler.cpp b/src/Archive/Formats/DiskArchiveHandler.cpp index b1bcf820a..bb83bdc66 100644 --- a/src/Archive/Formats/DiskArchiveHandler.cpp +++ b/src/Archive/Formats/DiskArchiveHandler.cpp @@ -54,7 +54,7 @@ using namespace slade; // Reads disk format data from a MemChunk. // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool DiskArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool DiskArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { size_t mcsize = mc.size(); @@ -127,7 +127,8 @@ bool DiskArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/DiskArchiveHandler.h b/src/Archive/Formats/DiskArchiveHandler.h index 0d18b6b5f..3c23fb889 100644 --- a/src/Archive/Formats/DiskArchiveHandler.h +++ b/src/Archive/Formats/DiskArchiveHandler.h @@ -18,8 +18,8 @@ class DiskArchiveHandler : public ArchiveFormatHandler ~DiskArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/GZipArchiveHandler.cpp b/src/Archive/Formats/GZipArchiveHandler.cpp index 33e3c84a6..1872e4113 100644 --- a/src/Archive/Formats/GZipArchiveHandler.cpp +++ b/src/Archive/Formats/GZipArchiveHandler.cpp @@ -53,7 +53,7 @@ using namespace slade; // Reads gzip format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool GZipArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool GZipArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Minimal metadata size is 18: 10 for header, 8 for footer size_t mds = 18; @@ -284,7 +284,7 @@ bool GZipArchiveHandler::loadEntryData(Archive& archive, const ArchiveEntry* ent // Returns the entry if it matches the search criteria in [options], or null // otherwise // ----------------------------------------------------------------------------- -ArchiveEntry* GZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* GZipArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables strutil::upperIP(options.match_name); @@ -325,7 +325,7 @@ ArchiveEntry* GZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptio // Returns the last entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* GZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* GZipArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options) { return findFirst(archive, options); } @@ -333,7 +333,7 @@ ArchiveEntry* GZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOption // ----------------------------------------------------------------------------- // Returns all entries matching the search criteria in [options] // ----------------------------------------------------------------------------- -vector GZipArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options) +vector GZipArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables vector ret; diff --git a/src/Archive/Formats/GZipArchiveHandler.h b/src/Archive/Formats/GZipArchiveHandler.h index fc2bd5335..6ac5fb7cd 100644 --- a/src/Archive/Formats/GZipArchiveHandler.h +++ b/src/Archive/Formats/GZipArchiveHandler.h @@ -11,7 +11,7 @@ class GZipArchiveHandler : public ArchiveFormatHandler ~GZipArchiveHandler() override = default; // Opening - bool open(Archive& archive, const MemChunk& mc) override; + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; @@ -47,9 +47,9 @@ class GZipArchiveHandler : public ArchiveFormatHandler } // Search - ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override; - ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override; - vector findAll(Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override; + vector findAll(const Archive& archive, ArchiveSearchOptions& options) override; // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/GobArchiveHandler.cpp b/src/Archive/Formats/GobArchiveHandler.cpp index 4dde7fdcb..37d7bee0c 100644 --- a/src/Archive/Formats/GobArchiveHandler.cpp +++ b/src/Archive/Formats/GobArchiveHandler.cpp @@ -49,7 +49,7 @@ using namespace slade; // Reads gob format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool GobArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool GobArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -132,7 +132,8 @@ bool GobArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/GobArchiveHandler.h b/src/Archive/Formats/GobArchiveHandler.h index 7a27c27e3..401b810eb 100644 --- a/src/Archive/Formats/GobArchiveHandler.h +++ b/src/Archive/Formats/GobArchiveHandler.h @@ -11,8 +11,8 @@ class GobArchiveHandler : public ArchiveFormatHandler ~GobArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/GrpArchiveHandler.cpp b/src/Archive/Formats/GrpArchiveHandler.cpp index d64b24513..1e5376c54 100644 --- a/src/Archive/Formats/GrpArchiveHandler.cpp +++ b/src/Archive/Formats/GrpArchiveHandler.cpp @@ -49,7 +49,7 @@ using namespace slade; // Reads grp format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -69,7 +69,7 @@ bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc) ken_magic[12] = 0; // Check the header - if (string_view{ ken_magic } != "KenSilverman") + if (string{ ken_magic } != "KenSilverman") { log::error("GrpArchiveHandler::openFile: File {} has invalid header", archive.filename()); global::error = "Invalid grp header"; @@ -129,7 +129,8 @@ bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); @@ -211,7 +212,7 @@ bool GrpArchiveHandler::isThisFormat(const MemChunk& mc) ken_magic[12] = 0; // Check the header - if (string_view{ ken_magic } != "KenSilverman") + if (string{ ken_magic } != "KenSilverman") return false; // Compute total size @@ -262,7 +263,7 @@ bool GrpArchiveHandler::isThisFormat(const string& filename) ken_magic[12] = 0; // Check the header - if (string_view{ ken_magic } != "KenSilverman") + if (string{ ken_magic } != "KenSilverman") return false; // Compute total size diff --git a/src/Archive/Formats/GrpArchiveHandler.h b/src/Archive/Formats/GrpArchiveHandler.h index bf502ed1c..ab3f3b63b 100644 --- a/src/Archive/Formats/GrpArchiveHandler.h +++ b/src/Archive/Formats/GrpArchiveHandler.h @@ -11,8 +11,8 @@ class GrpArchiveHandler : public ArchiveFormatHandler ~GrpArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/HogArchiveHandler.cpp b/src/Archive/Formats/HogArchiveHandler.cpp index 5ed77fee5..df8002185 100644 --- a/src/Archive/Formats/HogArchiveHandler.cpp +++ b/src/Archive/Formats/HogArchiveHandler.cpp @@ -116,7 +116,7 @@ bool shouldEncodeTxb(string_view name) // Reads hog format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool HogArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool HogArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -196,7 +196,8 @@ bool HogArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/HogArchiveHandler.h b/src/Archive/Formats/HogArchiveHandler.h index c6db82a2c..3eba1af74 100644 --- a/src/Archive/Formats/HogArchiveHandler.h +++ b/src/Archive/Formats/HogArchiveHandler.h @@ -11,8 +11,8 @@ class HogArchiveHandler : public ArchiveFormatHandler ~HogArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Entry addition/removal shared_ptr addEntry( diff --git a/src/Archive/Formats/LfdArchiveHandler.cpp b/src/Archive/Formats/LfdArchiveHandler.cpp index 56b46aaf2..b2b219663 100644 --- a/src/Archive/Formats/LfdArchiveHandler.cpp +++ b/src/Archive/Formats/LfdArchiveHandler.cpp @@ -50,7 +50,7 @@ using namespace slade; // Reads lfd format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool LfdArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool LfdArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -140,7 +140,8 @@ bool LfdArchiveHandler::open(Archive& archive, const MemChunk& mc) log::warning("Computed {} lumps, but actually {} entries", num_lumps, archive.numEntries()); // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/LfdArchiveHandler.h b/src/Archive/Formats/LfdArchiveHandler.h index 33fec39ef..c433d81f7 100644 --- a/src/Archive/Formats/LfdArchiveHandler.h +++ b/src/Archive/Formats/LfdArchiveHandler.h @@ -11,8 +11,8 @@ class LfdArchiveHandler : public ArchiveFormatHandler ~LfdArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/LibArchiveHandler.cpp b/src/Archive/Formats/LibArchiveHandler.cpp index c00c72a89..3daaef609 100644 --- a/src/Archive/Formats/LibArchiveHandler.cpp +++ b/src/Archive/Formats/LibArchiveHandler.cpp @@ -49,7 +49,7 @@ using namespace slade; // Reads wad format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool LibArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool LibArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -112,7 +112,8 @@ bool LibArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/LibArchiveHandler.h b/src/Archive/Formats/LibArchiveHandler.h index 50a2d80f7..ac3b82b7f 100644 --- a/src/Archive/Formats/LibArchiveHandler.h +++ b/src/Archive/Formats/LibArchiveHandler.h @@ -11,8 +11,8 @@ class LibArchiveHandler : public ArchiveFormatHandler ~LibArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/PakArchiveHandler.cpp b/src/Archive/Formats/PakArchiveHandler.cpp index a8565f102..c09d7695c 100644 --- a/src/Archive/Formats/PakArchiveHandler.cpp +++ b/src/Archive/Formats/PakArchiveHandler.cpp @@ -50,7 +50,7 @@ using namespace slade; // Reads pak format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool PakArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool PakArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check given data is valid if (mc.size() < 12) @@ -124,7 +124,8 @@ bool PakArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/PakArchiveHandler.h b/src/Archive/Formats/PakArchiveHandler.h index 151191f8c..d88f0d390 100644 --- a/src/Archive/Formats/PakArchiveHandler.h +++ b/src/Archive/Formats/PakArchiveHandler.h @@ -11,8 +11,8 @@ class PakArchiveHandler : public ArchiveFormatHandler ~PakArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/PodArchiveHandler.cpp b/src/Archive/Formats/PodArchiveHandler.cpp index 7feba4b5c..53de93ab9 100644 --- a/src/Archive/Formats/PodArchiveHandler.cpp +++ b/src/Archive/Formats/PodArchiveHandler.cpp @@ -70,7 +70,7 @@ void PodArchiveHandler::setId(string_view id) // Reads pod format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool PodArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool PodArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -113,7 +113,8 @@ bool PodArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/PodArchiveHandler.h b/src/Archive/Formats/PodArchiveHandler.h index 871b8750d..9c7d7b693 100644 --- a/src/Archive/Formats/PodArchiveHandler.h +++ b/src/Archive/Formats/PodArchiveHandler.h @@ -14,7 +14,7 @@ class PodArchiveHandler : public ArchiveFormatHandler void setId(string_view id); // Opening - bool open(Archive& archive, const MemChunk& mc) override; + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; diff --git a/src/Archive/Formats/ResArchiveHandler.cpp b/src/Archive/Formats/ResArchiveHandler.cpp index c13581dd3..09a0187fe 100644 --- a/src/Archive/Formats/ResArchiveHandler.cpp +++ b/src/Archive/Formats/ResArchiveHandler.cpp @@ -154,9 +154,6 @@ bool ResArchiveHandler::readDirectory( { parent->addEntry(nlump); - // Detect entry type - EntryType::detectEntryType(*nlump); - // Set entry to unchanged nlump->setState(EntryState::Unmodified); } @@ -168,7 +165,7 @@ bool ResArchiveHandler::readDirectory( // Reads res format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ResArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool ResArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -212,6 +209,14 @@ bool ResArchiveHandler::open(Archive& archive, const MemChunk& mc) if (!readDirectory(archive, mc, dir_offset, num_lumps, archive.rootDir())) return false; + // Detect maps (will detect map entry types) + ui::setSplashProgressMessage("Detecting maps"); + detectMaps(archive); + + // Detect all entry types + if (detect_types) + archive.detectAllEntryTypes(); + // Setup variables sig_blocker.unblock(); archive.setModified(false); diff --git a/src/Archive/Formats/ResArchiveHandler.h b/src/Archive/Formats/ResArchiveHandler.h index ac3df1851..a64656e9c 100644 --- a/src/Archive/Formats/ResArchiveHandler.h +++ b/src/Archive/Formats/ResArchiveHandler.h @@ -17,8 +17,8 @@ class ResArchiveHandler : public ArchiveFormatHandler size_t dir_offset, size_t num_lumps, shared_ptr parent); - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/RffArchiveHandler.cpp b/src/Archive/Formats/RffArchiveHandler.cpp index ac36af87f..0219a2f10 100644 --- a/src/Archive/Formats/RffArchiveHandler.cpp +++ b/src/Archive/Formats/RffArchiveHandler.cpp @@ -120,7 +120,7 @@ void bloodCrypt(void* data, int key, int len) // Reads rff format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool RffArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool RffArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -226,7 +226,8 @@ bool RffArchiveHandler::open(Archive& archive, const MemChunk& mc) delete[] lumps; // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/RffArchiveHandler.h b/src/Archive/Formats/RffArchiveHandler.h index 26a65eecc..1b64c37f3 100644 --- a/src/Archive/Formats/RffArchiveHandler.h +++ b/src/Archive/Formats/RffArchiveHandler.h @@ -11,8 +11,8 @@ class RffArchiveHandler : public ArchiveFormatHandler ~RffArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/SiNArchiveHandler.cpp b/src/Archive/Formats/SiNArchiveHandler.cpp index b381df708..587f2d448 100644 --- a/src/Archive/Formats/SiNArchiveHandler.cpp +++ b/src/Archive/Formats/SiNArchiveHandler.cpp @@ -51,7 +51,7 @@ using namespace slade; // Reads SiN format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool SiNArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool SiNArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check given data is valid if (mc.size() < 12) @@ -125,7 +125,8 @@ bool SiNArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/SiNArchiveHandler.h b/src/Archive/Formats/SiNArchiveHandler.h index a02f38d9d..51b05b1ee 100644 --- a/src/Archive/Formats/SiNArchiveHandler.h +++ b/src/Archive/Formats/SiNArchiveHandler.h @@ -11,8 +11,8 @@ class SiNArchiveHandler : public ArchiveFormatHandler ~SiNArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/TarArchiveHandler.cpp b/src/Archive/Formats/TarArchiveHandler.cpp index f0a11ce2b..72e0644d4 100644 --- a/src/Archive/Formats/TarArchiveHandler.cpp +++ b/src/Archive/Formats/TarArchiveHandler.cpp @@ -259,7 +259,7 @@ void tarDefaultHeader(TarHeader* header) // Reads tar format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool TarArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool TarArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check given data is valid if (mc.size() < 1024) @@ -356,7 +356,8 @@ bool TarArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/TarArchiveHandler.h b/src/Archive/Formats/TarArchiveHandler.h index 8dc986447..aa2edfcdb 100644 --- a/src/Archive/Formats/TarArchiveHandler.h +++ b/src/Archive/Formats/TarArchiveHandler.h @@ -11,8 +11,8 @@ class TarArchiveHandler : public ArchiveFormatHandler ~TarArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/Wad2ArchiveHandler.cpp b/src/Archive/Formats/Wad2ArchiveHandler.cpp index c80441772..0e3339364 100644 --- a/src/Archive/Formats/Wad2ArchiveHandler.cpp +++ b/src/Archive/Formats/Wad2ArchiveHandler.cpp @@ -51,7 +51,7 @@ using namespace slade; // Reads wad format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool Wad2ArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool Wad2ArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -128,7 +128,12 @@ bool Wad2ArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); + + // Detect maps (will detect map entry types) + ui::setSplashProgressMessage("Detecting maps"); + detectMaps(archive); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/Wad2ArchiveHandler.h b/src/Archive/Formats/Wad2ArchiveHandler.h index 98abfd387..cec3c5c8c 100644 --- a/src/Archive/Formats/Wad2ArchiveHandler.h +++ b/src/Archive/Formats/Wad2ArchiveHandler.h @@ -23,7 +23,7 @@ class Wad2ArchiveHandler : public ArchiveFormatHandler ~Wad2ArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk // Format detection diff --git a/src/Archive/Formats/WadArchiveHandler.cpp b/src/Archive/Formats/WadArchiveHandler.cpp index a2cb36c95..8c91ef9bb 100644 --- a/src/Archive/Formats/WadArchiveHandler.cpp +++ b/src/Archive/Formats/WadArchiveHandler.cpp @@ -151,8 +151,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive) // Check for namespace begin if (strutil::endsWith(entry->upperName(), "_START")) { - log::debug("Found namespace start marker {} at index {}", entry->name(), archive.entryIndex(entry)); - // Create new namespace NSPair ns(entry, nullptr); string_view name = entry->name(); @@ -170,8 +168,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive) if (ns.name == "tt") ns.name = "t"; - log::debug("Added namespace {}", ns.name); - // Add to namespace list namespaces_.push_back(ns); } @@ -179,8 +175,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive) // else if (strutil::matches(entry->upperName(), "?_END") || strutil::matches(entry->upperName(), "??_END")) else if (strutil::endsWith(entry->upperName(), "_END")) { - log::debug("Found namespace end marker {} at index {}", entry->name(), archive.entryIndex(entry)); - // Get namespace 'name' auto ns_name = strutil::lower(entry->name()); strutil::removeLastIP(ns_name, 4); @@ -195,8 +189,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive) if (ns_name == "tt") ns_name = "t"; - log::debug("Namespace name {}", ns_name); - // Check if it's the end of an existing namespace // Remember entry is getEntry(a)? index is 'a' // size_t index = entryIndex(entry); @@ -290,7 +282,7 @@ bool WadArchiveHandler::hasFlatHack() // Reads wad format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool WadArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool WadArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -459,7 +451,8 @@ bool WadArchiveHandler::open(Archive& archive, const MemChunk& mc) updateNamespaces(archive); // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Identify #included lumps (DECORATE, GLDEFS, etc.) detectIncludes(archive); @@ -766,7 +759,7 @@ bool WadArchiveHandler::moveEntry(Archive& archive, ArchiveEntry* entry, unsigne // If [maphead] is not really a map header entry, an invalid MapDesc will be // returned (MapDesc::head == nullptr) // ----------------------------------------------------------------------------- -MapDesc WadArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) +MapDesc WadArchiveHandler::mapDesc(const Archive& archive, ArchiveEntry* maphead) { MapDesc map; @@ -910,7 +903,7 @@ MapDesc WadArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) // ----------------------------------------------------------------------------- // Searches for any maps in the wad and adds them to the map list // ----------------------------------------------------------------------------- -vector WadArchiveHandler::detectMaps(Archive& archive) +vector WadArchiveHandler::detectMaps(const Archive& archive) { vector maps; @@ -1043,8 +1036,8 @@ vector WadArchiveHandler::detectMaps(Archive& archive) if (entry->type()->formatId() == "archive_wad") { // Detect map format (probably kinda slow but whatever, no better way to do it really) - Archive tempwad("wad"); - tempwad.open(entry->data()); + Archive tempwad(ArchiveFormat::Wad); + tempwad.open(entry->data(), true); auto emaps = tempwad.detectMaps(); if (!emaps.empty()) { @@ -1078,7 +1071,7 @@ vector WadArchiveHandler::detectMaps(Archive& archive) // ----------------------------------------------------------------------------- // Returns the namespace that [entry] is within // ----------------------------------------------------------------------------- -string WadArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry) +string WadArchiveHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry) { return detectNamespace(archive, archive.entryIndex(entry)); } @@ -1086,7 +1079,7 @@ string WadArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry) // ----------------------------------------------------------------------------- // Returns the namespace that the entry at [index] in [dir] is within // ----------------------------------------------------------------------------- -string WadArchiveHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir) +string WadArchiveHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir) { // Go through namespaces for (auto& ns : namespaces_) @@ -1162,7 +1155,7 @@ void WadArchiveHandler::detectIncludes(Archive& archive) // Returns the first entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* WadArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* WadArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables unsigned index = 0; @@ -1231,7 +1224,7 @@ ArchiveEntry* WadArchiveHandler::findFirst(Archive& archive, ArchiveSearchOption // Returns the last entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* WadArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* WadArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables int index = archive.numEntries() - 1; @@ -1303,7 +1296,7 @@ ArchiveEntry* WadArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions // ----------------------------------------------------------------------------- // Returns all entries matching the search criteria in [options] // ----------------------------------------------------------------------------- -vector WadArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options) +vector WadArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables unsigned index = 0; diff --git a/src/Archive/Formats/WadArchiveHandler.h b/src/Archive/Formats/WadArchiveHandler.h index 1af23f849..2ed53c0b5 100644 --- a/src/Archive/Formats/WadArchiveHandler.h +++ b/src/Archive/Formats/WadArchiveHandler.h @@ -16,7 +16,7 @@ class WadArchiveHandler : public ArchiveFormatHandler void updateNamespaces(Archive& archive); // Opening - bool open(Archive& archive, const MemChunk& mc) override; + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk @@ -41,17 +41,17 @@ class WadArchiveHandler : public ArchiveFormatHandler override; // Detection - MapDesc mapDesc(Archive& archive, ArchiveEntry* maphead) override; - vector detectMaps(Archive& archive) override; - string detectNamespace(Archive& archive, ArchiveEntry* entry) override; - string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override; + MapDesc mapDesc(const Archive& archive, ArchiveEntry* maphead) override; + vector detectMaps(const Archive& archive) override; + string detectNamespace(const Archive& archive, ArchiveEntry* entry) override; + string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override; void detectIncludes(Archive& archive); bool hasFlatHack() override; // Search - ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override; - ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override; - vector findAll(Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override; + vector findAll(const Archive& archive, ArchiveSearchOptions& options) override; // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/WadJArchiveHandler.cpp b/src/Archive/Formats/WadJArchiveHandler.cpp index 28b8dbb95..92d20e7f0 100644 --- a/src/Archive/Formats/WadJArchiveHandler.cpp +++ b/src/Archive/Formats/WadJArchiveHandler.cpp @@ -65,7 +65,7 @@ WadJArchiveHandler::WadJArchiveHandler() : WadArchiveHandler(ArchiveFormat::WadJ // Reads wad format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool WadJArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool WadJArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -204,7 +204,8 @@ bool WadJArchiveHandler::open(Archive& archive, const MemChunk& mc) updateNamespaces(archive); // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Detect maps (will detect map entry types) ui::setSplashProgressMessage("Detecting maps"); @@ -284,14 +285,14 @@ bool WadJArchiveHandler::write(Archive& archive, MemChunk& mc) // ----------------------------------------------------------------------------- // Hack to account for Jaguar Doom's silly sprite scheme // ----------------------------------------------------------------------------- -string WadJArchiveHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir) +string WadJArchiveHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir) { auto nextentry = archive.entryAt(index + 1); if (nextentry && strutil::equalCI(nextentry->name(), ".")) return "sprites"; return WadArchiveHandler::detectNamespace(archive, index); } -string WadJArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry) +string WadJArchiveHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry) { size_t index = archive.entryIndex(entry); auto nextentry = archive.entryAt(index + 1); diff --git a/src/Archive/Formats/WadJArchiveHandler.h b/src/Archive/Formats/WadJArchiveHandler.h index 60892a58d..e1a0f2b1b 100644 --- a/src/Archive/Formats/WadJArchiveHandler.h +++ b/src/Archive/Formats/WadJArchiveHandler.h @@ -11,11 +11,11 @@ class WadJArchiveHandler : public WadArchiveHandler ~WadJArchiveHandler() override = default; // Opening/writing - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk - bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk + bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk - string detectNamespace(Archive& archive, ArchiveEntry* entry) override; - string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override; + string detectNamespace(const Archive& archive, ArchiveEntry* entry) override; + string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override; // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/Archive/Formats/WolfArchiveHandler.cpp b/src/Archive/Formats/WolfArchiveHandler.cpp index 0a1919621..3ed90b06c 100644 --- a/src/Archive/Formats/WolfArchiveHandler.cpp +++ b/src/Archive/Formats/WolfArchiveHandler.cpp @@ -34,7 +34,6 @@ #include "Archive/Archive.h" #include "Archive/ArchiveDir.h" #include "Archive/ArchiveEntry.h" -#include "Archive/EntryType/EntryType.h" #include "General/UI.h" #include "Utility/FileUtils.h" #include "Utility/StringUtils.h" @@ -372,7 +371,7 @@ void expandWolfGraphLump(ArchiveEntry* entry, size_t lumpnum, size_t numlumps, H // Reads a Wolf format file from disk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool WolfArchiveHandler::open(Archive& archive, string_view filename) +bool WolfArchiveHandler::open(Archive& archive, string_view filename, bool detect_types) { // Find wolf archive type strutil::Path fn1(filename); @@ -395,7 +394,7 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename) MemChunk data, head; head.importFile(findFileCasing(fn1)); data.importFile(findFileCasing(fn2)); - opened = openMaps(archive, head, data); + opened = openMaps(archive, head, data, detect_types); } else if (fn1_name == "AUDIOHED" || fn1_name == "AUDIOT") { @@ -405,7 +404,7 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename) MemChunk data, head; head.importFile(findFileCasing(fn1)); data.importFile(findFileCasing(fn2)); - opened = openAudio(archive, head, data); + opened = openAudio(archive, head, data, detect_types); } else if (fn1_name == "VGAHEAD" || fn1_name == "VGAGRAPH" || fn1_name == "VGADICT") { @@ -418,7 +417,7 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename) head.importFile(findFileCasing(fn1)); data.importFile(findFileCasing(fn2)); dict.importFile(findFileCasing(fn3)); - opened = openGraph(archive, head, data, dict); + opened = openGraph(archive, head, data, dict, detect_types); } else { @@ -430,17 +429,26 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename) return false; } // Load from MemChunk - opened = open(archive, mc); + opened = open(archive, mc, detect_types); } - return opened; + if (opened) + { + // Detect all entry types + if (detect_types) + archive.detectAllEntryTypes(); + + return true; + } + else + return false; } // ----------------------------------------------------------------------------- // Reads VSWAP Wolf format data from a MemChunk. // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Check data was given if (!mc.hasData()) @@ -552,7 +560,8 @@ bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc) } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); @@ -567,7 +576,7 @@ bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc) // Reads Wolf AUDIOT/AUDIOHEAD format data from a MemChunk. // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemChunk& data) +bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types) { // Check data was given if (!head.hasData() || !data.hasData()) @@ -689,16 +698,19 @@ bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemCh nlump->setOffsetOnDisk(offset); nlump->setSizeOnDisk(); - // Detect entry type + // Load data if (size > 0) nlump->importMemChunk(edata); - EntryType::detectEntryType(*nlump); // Add to entry list nlump->setState(EntryState::Unmodified); archive.rootDir()->addEntry(nlump); } + // Detect all entry types + if (detect_types) + archive.detectAllEntryTypes(); + // Setup variables sig_blocker.unblock(); archive.setModified(false); @@ -712,7 +724,7 @@ bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemCh // Reads Wolf GAMEMAPS/MAPHEAD format data from a MemChunk. // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChunk& data) +bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types) { // Check data was given if (!head.hasData() || !data.hasData()) @@ -790,7 +802,8 @@ bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChu } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); @@ -806,7 +819,12 @@ bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChu // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- #define WC(a) WolfConstant(a, num_lumps) -bool WolfArchiveHandler::openGraph(Archive& archive, const MemChunk& head, const MemChunk& data, MemChunk& dict) +bool WolfArchiveHandler::openGraph( + Archive& archive, + const MemChunk& head, + const MemChunk& data, + MemChunk& dict, + bool detect_types) { // Check data was given if (!head.hasData() || !data.hasData() || !dict.hasData()) @@ -905,7 +923,8 @@ bool WolfArchiveHandler::openGraph(Archive& archive, const MemChunk& head, const } // Detect all entry types - detectAllEntryTypes(archive); + if (detect_types) + archive.detectAllEntryTypes(); // Setup variables sig_blocker.unblock(); diff --git a/src/Archive/Formats/WolfArchiveHandler.h b/src/Archive/Formats/WolfArchiveHandler.h index 720add501..164cc3ad1 100644 --- a/src/Archive/Formats/WolfArchiveHandler.h +++ b/src/Archive/Formats/WolfArchiveHandler.h @@ -11,12 +11,12 @@ class WolfArchiveHandler : public ArchiveFormatHandler ~WolfArchiveHandler() override = default; // Opening - bool open(Archive& archive, string_view filename) override; // Open from File - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk + bool open(Archive& archive, string_view filename, bool detect_types) override; // Open from File + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk - bool openAudio(Archive& archive, MemChunk& head, const MemChunk& data); - bool openGraph(Archive& archive, const MemChunk& head, const MemChunk& data, MemChunk& dict); - bool openMaps(Archive& archive, MemChunk& head, const MemChunk& data); + bool openAudio(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types); + bool openGraph(Archive& archive, const MemChunk& head, const MemChunk& data, MemChunk& dict, bool detect_types); + bool openMaps(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types); // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk diff --git a/src/Archive/Formats/ZipArchiveHandler.cpp b/src/Archive/Formats/ZipArchiveHandler.cpp index d05cf3aa3..1d9e0d746 100644 --- a/src/Archive/Formats/ZipArchiveHandler.cpp +++ b/src/Archive/Formats/ZipArchiveHandler.cpp @@ -93,7 +93,7 @@ void ZipArchiveHandler::init(Archive& archive) // Reads zip data from a file // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ZipArchiveHandler::open(Archive& archive, string_view filename) +bool ZipArchiveHandler::open(Archive& archive, string_view filename, bool detect_types) { // Check the file exists if (!fileutil::fileExists(filename)) @@ -162,9 +162,6 @@ bool ZipArchiveHandler::open(Archive& archive, string_view filename) zip.Read(data.data(), ze_size); // Note: this is where exceedingly large files cause an exception. new_entry->importMem(data.data(), static_cast(ze_size)); } - - // Determine its type - EntryType::detectEntryType(*new_entry); } else { @@ -192,6 +189,10 @@ bool ZipArchiveHandler::open(Archive& archive, string_view filename) for (auto& entry : entry_list) entry->setState(EntryState::Unmodified); + // Detect all entry types + if (detect_types) + archive.detectAllEntryTypes(); + // Enable announcements sig_blocker.unblock(); @@ -206,14 +207,14 @@ bool ZipArchiveHandler::open(Archive& archive, string_view filename) // Reads zip format data from a MemChunk // Returns true if successful, false otherwise // ----------------------------------------------------------------------------- -bool ZipArchiveHandler::open(Archive& archive, const MemChunk& mc) +bool ZipArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types) { // Write the MemChunk to a temp file const auto tempfile = app::path("slade-temp-open.zip", app::Dir::Temp); mc.exportFile(tempfile); // Load the file - const bool success = open(archive, tempfile); + const bool success = open(archive, tempfile, true); // Clean up fileutil::removeFile(tempfile); @@ -468,7 +469,7 @@ shared_ptr ZipArchiveHandler::addEntry( // Returns the mapdesc_t information about the map at [entry], if [entry] is // actually a valid map (ie. a wad archive in the maps folder) // ----------------------------------------------------------------------------- -MapDesc ZipArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) +MapDesc ZipArchiveHandler::mapDesc(const Archive& archive, ArchiveEntry* maphead) { MapDesc map; @@ -484,11 +485,20 @@ MapDesc ZipArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) if (maphead->parentDir()->parent() != archive.rootDir() || maphead->parentDir()->name() != "maps") return map; + // Detect map format + auto format = MapFormat::Unknown; + Archive tempwad(ArchiveFormat::Wad); + tempwad.open(maphead->data(), true); + auto emaps = tempwad.detectMaps(); + if (!emaps.empty()) + format = emaps[0].format; + // Setup map info map.archive = true; map.head = maphead->getShared(); map.end = maphead->getShared(); map.name = maphead->upperNameNoExt(); + map.format = format; return map; } @@ -497,7 +507,7 @@ MapDesc ZipArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead) // Detects all the maps in the archive and returns a vector of information about // them. // ----------------------------------------------------------------------------- -vector ZipArchiveHandler::detectMaps(Archive& archive) +vector ZipArchiveHandler::detectMaps(const Archive& archive) { vector ret; @@ -518,7 +528,7 @@ vector ZipArchiveHandler::detectMaps(Archive& archive) // Detect map format (probably kinda slow but whatever, no better way to do it really) auto format = MapFormat::Unknown; Archive tempwad(ArchiveFormat::Wad); - tempwad.open(entry->data()); + tempwad.open(entry->data(), true); auto emaps = tempwad.detectMaps(); if (!emaps.empty()) format = emaps[0].format; @@ -540,7 +550,7 @@ vector ZipArchiveHandler::detectMaps(Archive& archive) // Returns the first entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* ZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* ZipArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = archive.rootDir().get(); @@ -573,7 +583,7 @@ ArchiveEntry* ZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOption // Returns the last entry matching the search criteria in [options], or null if // no matching entry was found // ----------------------------------------------------------------------------- -ArchiveEntry* ZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options) +ArchiveEntry* ZipArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = archive.rootDir().get(); @@ -605,7 +615,7 @@ ArchiveEntry* ZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions // ----------------------------------------------------------------------------- // Returns all entries matching the search criteria in [options] // ----------------------------------------------------------------------------- -vector ZipArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options) +vector ZipArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options) { // Init search variables auto dir = archive.rootDir().get(); diff --git a/src/Archive/Formats/ZipArchiveHandler.h b/src/Archive/Formats/ZipArchiveHandler.h index 5ea8170bd..3d987a5d9 100644 --- a/src/Archive/Formats/ZipArchiveHandler.h +++ b/src/Archive/Formats/ZipArchiveHandler.h @@ -13,8 +13,8 @@ class ZipArchiveHandler : public ArchiveFormatHandler void init(Archive& archive) override; // Opening - bool open(Archive& archive, string_view filename) override; // Open from File - bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk + bool open(Archive& archive, string_view filename, bool detect_types) override; // Open from File + bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk // Writing/Saving bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk @@ -28,13 +28,13 @@ class ZipArchiveHandler : public ArchiveFormatHandler override; // Detection - MapDesc mapDesc(Archive& archive, ArchiveEntry* maphead) override; - vector detectMaps(Archive& archive) override; + MapDesc mapDesc(const Archive& archive, ArchiveEntry* maphead) override; + vector detectMaps(const Archive& archive) override; // Search - ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override; - ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override; - vector findAll(Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override; + ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override; + vector findAll(const Archive& archive, ArchiveSearchOptions& options) override; // Format detection bool isThisFormat(const MemChunk& mc) override; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c7527d3e..0edc8fc6e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,10 +9,12 @@ file(GLOB_RECURSE SLADE_SOURCES CONFIGURE_DEPENDS Application/*.cpp Archive/*.cpp Audio/*.cpp + Database/*.cpp Game/*.cpp General/*.cpp Geometry/*.cpp Graphics/*.cpp + Library/*.cpp MainEditor/*.cpp MapEditor/*.cpp OpenGL/*.cpp diff --git a/src/Database/Context.cpp b/src/Database/Context.cpp new file mode 100644 index 000000000..546b8ccb7 --- /dev/null +++ b/src/Database/Context.cpp @@ -0,0 +1,274 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: Context.cpp +// Description: Database Context class - keeps connections open to a database, +// since opening a new connection is expensive. It can also keep +// cached sql queries (for frequent reuse) +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "Context.h" +#include "App.h" +#include "Transaction.h" +#include +#include + +using namespace slade; +using namespace database; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::database +{ +Context db_global; +vector thread_contexts; +std::shared_mutex mutex_thread_contexts; +} // namespace slade::database + + +// ----------------------------------------------------------------------------- +// +// Context Class Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Context class constructor +// ----------------------------------------------------------------------------- +Context::Context(string_view file_path) +{ + thread_id_ = std::this_thread::get_id(); + + if (!file_path.empty()) + open(file_path); +} + +// ----------------------------------------------------------------------------- +// Context class destructor +// ----------------------------------------------------------------------------- +Context::~Context() +{ + close(); + + for (int i = static_cast(thread_contexts.size()) - 1; i >= 0; i--) + if (thread_contexts[i] == this) + thread_contexts.erase(thread_contexts.begin() + i); +} + +// ----------------------------------------------------------------------------- +// Returns true if the context was created on the current thread +// ----------------------------------------------------------------------------- +bool Context::isForThisThread() const +{ + return thread_id_ == std::this_thread::get_id(); +} + +// ----------------------------------------------------------------------------- +// Opens connections to the database file at [file_path]. +// Returns false if any existing connections couldn't be closed, true otherwise +// ----------------------------------------------------------------------------- +bool Context::open(string_view file_path) +{ + if (!close()) + return false; + + file_path_ = file_path; + connection_ro_ = std::make_unique(file_path_, SQLite::OPEN_READONLY, 100); + connection_rw_ = std::make_unique(file_path_, SQLite::OPEN_READWRITE, 100); + + return true; +} + +// ----------------------------------------------------------------------------- +// Closes the context's connections to its database +// ----------------------------------------------------------------------------- +bool Context::close() +{ + if (!connection_ro_) + return true; + + try + { + cached_queries_.clear(); + connection_ro_ = nullptr; + connection_rw_ = nullptr; + } + catch (std::exception& ex) + { + log::error("Error closing connections for database {}: {}", file_path_, ex.what()); + return false; + } + + file_path_.clear(); + return true; +} + +// ----------------------------------------------------------------------------- +// Returns the cached query [id] or nullptr if not found +// ----------------------------------------------------------------------------- +Statement* Context::cachedQuery(string_view id) +{ + auto i = cached_queries_.find(id); + if (i != cached_queries_.end()) + { + i->second->tryReset(); + return i->second.get(); + } + + return nullptr; +} + +// ----------------------------------------------------------------------------- +// Returns the cached query [id] if it exists, otherwise creates a new cached +// query from the given [sql] string and returns it. +// If [writes] is true, the created query will use the read+write connection. +// ----------------------------------------------------------------------------- +Statement* Context::cacheQuery(string_view id, string_view sql, bool writes) +{ + // Check for existing cached query [id] + auto i = cached_queries_.find(id); + if (i != cached_queries_.end()) + { + i->second->tryReset(); + return i->second.get(); + } + + // Check connection + if (!connection_ro_) + return nullptr; + + // Create & add cached query + auto& db = writes ? *connection_rw_ : *connection_ro_; + auto statement = std::make_unique(db, sql); + auto ptr = statement.get(); + cached_queries_.emplace(id, std::move(statement)); + + return ptr; +} + +// ----------------------------------------------------------------------------- +// Executes an sql [query] on the database. +// Returns the number of rows modified/created by the query, or 0 if the context +// is not connected +// ----------------------------------------------------------------------------- +int Context::exec(const string& query) const +{ + return connection_rw_ ? connection_rw_->exec(query) : 0; +} +int Context::exec(const char* query) const +{ + return connection_rw_ ? connection_rw_->exec(query) : 0; +} + +// ----------------------------------------------------------------------------- +// Returns true if a row exists in [table_name] where [id_col] = [id]. +// The column must be an integer column for this to work correctly +// ----------------------------------------------------------------------------- +bool Context::rowIdExists(string_view table_name, int64_t id, string_view id_col) const +{ + auto query = fmt::format("SELECT EXISTS(SELECT 1 FROM {} WHERE {} = {})", table_name, id_col, id); + return connection_ro_->execAndGet(query).getInt() > 0; +} + +// ----------------------------------------------------------------------------- +// Begins a transaction and returns a Transaction object to encapsulate it +// ----------------------------------------------------------------------------- +Transaction Context::beginTransaction(bool write) const +{ + return { write ? connection_rw_.get() : connection_ro_.get(), true }; +} + +// ----------------------------------------------------------------------------- +// Cleans up the database file to reduce size on disk +// ----------------------------------------------------------------------------- +void Context::vacuum() const +{ + exec("VACUUM;"); +} + + +// ----------------------------------------------------------------------------- +// +// Database Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Returns the 'global' database connection context for this thread. +// +// If this isn't being called from the main thread, it will first look for a +// context that has previously been registered for the current thread via +// registerThreadContext. If no context has been registered for the thread, the +// main thread's context will be returned and a warning logged +// ----------------------------------------------------------------------------- +Context& database::global() +{ + // Check if we are not on the main thread + if (std::this_thread::get_id() != app::mainThreadId()) + { + std::shared_lock lock(mutex_thread_contexts); + + // Find context for this thread + for (auto* thread_context : thread_contexts) + if (thread_context->isForThisThread()) + return *thread_context; + + // No context available for this thread, warn and use main thread context + // (should this throw an exception?) + log::warning("A non-main thread is requesting the global database connection context"); + } + + return db_global; +} + +// ----------------------------------------------------------------------------- +// Sets [context] as the database connection context to use for the current +// thread when calling database::global() +// ----------------------------------------------------------------------------- +void database::registerThreadContext(Context& context) +{ + std::unique_lock lock(mutex_thread_contexts); + + thread_contexts.push_back(&context); +} + +// ----------------------------------------------------------------------------- +// Clears all contexts registered for the current thread +// ----------------------------------------------------------------------------- +void database::deregisterThreadContexts() +{ + std::unique_lock lock(mutex_thread_contexts); + + for (int i = static_cast(thread_contexts.size()) - 1; i >= 0; i--) + if (thread_contexts[i]->isForThisThread()) + thread_contexts.erase(thread_contexts.begin() + i); +} diff --git a/src/Database/Context.h b/src/Database/Context.h new file mode 100644 index 000000000..9b355f4a0 --- /dev/null +++ b/src/Database/Context.h @@ -0,0 +1,62 @@ +#pragma once + +#include "Statement.h" +#include + +namespace SQLite +{ +class Database; +} + +namespace slade::database +{ +class Transaction; + +class Context +{ +public: + Context(string_view file_path = {}); + ~Context(); + + const string& filePath() const { return file_path_; } + SQLite::Database* connectionRO() const { return connection_ro_.get(); } + SQLite::Database* connectionRW() const { return connection_rw_.get(); } + + bool isOpen() const { return connection_ro_.get(); } + bool isForThisThread() const; + + bool open(string_view file_path); + bool close(); + + Statement* cachedQuery(string_view id); + Statement* cacheQuery(string_view id, string_view sql, bool writes = false); + + int exec(const string& query) const; + int exec(const char* query) const; + + bool rowIdExists(string_view table_name, int64_t id, string_view id_col = "id") const; + + Transaction beginTransaction(bool write = false) const; + + void vacuum() const; + +private: + string file_path_; + std::thread::id thread_id_; + + unique_ptr connection_ro_; + unique_ptr connection_rw_; + + std::map, std::less<>> cached_queries_; +}; + +// Global (config) database context +Context& global(); + +// Contexts for threads +void registerThreadContext(Context& context); +void deregisterThreadContexts(); +} // namespace slade::database + +// Shortcut alias for database namespace +namespace db = slade::database; diff --git a/src/Database/Database.cpp b/src/Database/Database.cpp new file mode 100644 index 000000000..a39ae5142 --- /dev/null +++ b/src/Database/Database.cpp @@ -0,0 +1,594 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: Database.cpp +// Description: Functions for working with the SLADE program database +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "Database.h" +#include "App.h" +#include "Archive/Archive.h" +#include "Archive/ArchiveDir.h" +#include "Archive/ArchiveEntry.h" +#include "Context.h" +#include "General/Console.h" +#include "General/UI.h" +#include "UI/State.h" +#include "Utility/DateTime.h" +#include "Utility/FileUtils.h" +#include "Utility/StringUtils.h" +#include "Utility/Tokenizer.h" +#include +#include + +using namespace slade; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::database +{ +int db_version = 1; +int64_t session_id = -1; +} // namespace slade::database + + +// ----------------------------------------------------------------------------- +// +// Database Namespace Functions +// +// ----------------------------------------------------------------------------- +namespace slade::database +{ +void migrateWindowLayout(string_view filename, const char* window_id) +{ + // Open layout file + Tokenizer tz; + if (!tz.openFile(app::path(filename, app::Dir::User))) + return; + + // Parse layout + vector layouts; + while (true) + { + // Read component+layout pair + auto component = tz.getToken(); + auto layout = tz.getToken(); + layouts.emplace_back(component, layout); + + // Check if we're done + if (tz.peekToken().empty()) + break; + } + + ui::setWindowLayout(window_id, layouts); +} + +// ----------------------------------------------------------------------------- +// Creates any missing tables/views in the SLADE database [db] +// ----------------------------------------------------------------------------- +bool createMissingTables(SQLite::Database& db) +{ + // Get slade.pk3 dir with table definition scripts + auto tables_dir = app::programResource()->dirAtPath("database/tables"); + if (!tables_dir) + { + global::error = "Unable to initialize SLADE database: no table definitions in slade.pk3"; + return false; + } + + for (const auto& entry : tables_dir->entries()) + { + // Check table exists + string table_name{ strutil::Path::fileNameOf(entry->name(), false) }; + if (db.tableExists(table_name)) + continue; + + // Doesn't exist, create table + string sql{ reinterpret_cast(entry->data().data()), entry->data().size() }; + try + { + db.exec(sql); + log::info("Created database table {}", table_name); + } + catch (const SQLite::Exception& ex) + { + global::error = fmt::format("Failed to create database table {}: {}", table_name, ex.what()); + return false; + } + } + + // Get slade.pk3 dir with view definition scripts + auto views_dir = app::programResource()->dirAtPath("database/views"); + if (views_dir) + { + for (const auto& entry : views_dir->entries()) + { + // Check view exists + string view_name{ strutil::Path::fileNameOf(entry->name(), false) }; + if (viewExists(view_name, db)) + continue; + + // Doesn't exist, create view + string sql{ reinterpret_cast(entry->data().data()), entry->data().size() }; + try + { + db.exec(sql); + log::info("Created database view {}", view_name); + } + catch (const SQLite::Exception& ex) + { + global::error = fmt::format("Failed to create database view {}: {}", view_name, ex.what()); + return false; + } + } + } + + return true; +} + +// ----------------------------------------------------------------------------- +// Creates and initializes a new program database file at [file_path] +// ----------------------------------------------------------------------------- +bool createDatabase(const string& file_path) +{ + SQLite::Database db(file_path, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + + // Create tables + if (!createMissingTables(db)) + return false; + + // Init db_info table + try + { + Statement sql{ db, "INSERT INTO db_info (version) VALUES (?)" }; + sql.bind(1, db_version); + sql.exec(); + } + catch (const SQLite::Exception& ex) + { + global::error = fmt::format("Failed to initialize database: {}", ex.what()); + log::error(global::error); + return false; + } + + return true; +} + +// ----------------------------------------------------------------------------- +// Updates the program database tables +// ----------------------------------------------------------------------------- +bool updateDatabase(int prev_version) +{ + log::info("Updating database from v{} to v{}...", prev_version, db_version); + + // Create missing tables + if (!createMissingTables(*connectionRW())) + return false; + + // Update db_info.version + exec(fmt::format("UPDATE db_info SET version = {}", db_version)); + + // Done + log::info("Database updated to v{} successfully", db_version); + return true; +} + +// ----------------------------------------------------------------------------- +// Writes a new session row to the database +// ----------------------------------------------------------------------------- +bool beginSession() +{ + try + { + Statement sql_new_session{ *connectionRW(), + "INSERT INTO session (opened_time, closed_time, version_major, version_minor, " + "version_revision, version_beta) " + "VALUES (?,?,?,?,?,?)" }; + + sql_new_session.bindDateTime(1, datetime::now()); + sql_new_session.bind(2); // NULL closed time + sql_new_session.bind(3, static_cast(app::version().major)); + sql_new_session.bind(4, static_cast(app::version().minor)); + sql_new_session.bind(5, static_cast(app::version().revision)); + sql_new_session.bind(6, static_cast(app::version().beta)); + + if (sql_new_session.exec() == 0) + { + global::error = "Failed to initialize database: Session row creation failed"; + return false; + } + + session_id = connectionRO()->execAndGet("SELECT MAX(id) FROM session").getInt64(); + + return true; + } + catch (const SQLite::Exception& ex) + { + global::error = fmt::format("Failed to initialize database: {}", ex.what()); + log::error(global::error); + return false; + } +} + +// ----------------------------------------------------------------------------- +// Sets the closed time in the current session row +// ----------------------------------------------------------------------------- +bool endSession() +{ + try + { + Statement sql_close_session{ *connectionRW(), "UPDATE session SET closed_time = ? WHERE id = ?" }; + sql_close_session.bindDateTime(1, datetime::now()); + sql_close_session.bind(2, session_id); + sql_close_session.exec(); + + session_id = -1; + + return true; + } + catch (const SQLite::Exception& ex) + { + log::error(ex.what()); + return false; + } +} +} // namespace slade::database + +// ----------------------------------------------------------------------------- +// Executes an sql [query] on the database using the given [connection]. +// If [connection] is null, the global read+write connection is used. +// Returns the number of rows modified/created by the query, or 0 if the global +// connection context is not connected +// ----------------------------------------------------------------------------- +int database::exec(const string& query, SQLite::Database* connection) +{ + if (!connection) + connection = connectionRW(); + if (!connection) + return 0; + + return connection->exec(query); +} +int database::exec(const char* query, SQLite::Database* connection) +{ + if (!connection) + connection = connectionRW(); + if (!connection) + return 0; + + return connection->exec(query); +} + +// ----------------------------------------------------------------------------- +// Returns true if a view with [view_name] exists in the database [connection] +// ----------------------------------------------------------------------------- +bool database::viewExists(string_view view_name, const SQLite::Database& connection) +{ + Statement query(connection, "SELECT count(*) FROM sqlite_master WHERE type='view' AND name=?"); + query.bind(1, view_name); + (void)query.executeStep(); // Cannot return false, as the above query always return a result + return (1 == query.getColumn(0).getInt()); +} + +// ----------------------------------------------------------------------------- +// Returns true if a row exists in [table_name] where [id_col] = [id]. +// The column must be an integer column for this to work correctly +// ----------------------------------------------------------------------------- +bool database::rowIdExists(string_view table_name, int64_t id, string_view id_col) +{ + return global().rowIdExists(table_name, id, id_col); +} + +// ----------------------------------------------------------------------------- +// Returns the cached query [id] if it exists, otherwise creates a new cached +// query from the given [sql] string and returns it. +// If [writes] is true, the created query will use the read+write connection. +// ----------------------------------------------------------------------------- +database::Statement* database::cacheQuery(string_view id, string_view sql, bool writes) +{ + return global().cacheQuery(id, sql, writes); +} + +// ----------------------------------------------------------------------------- +// Returns the read-only connection to the database (for the calling thread) +// ----------------------------------------------------------------------------- +SQLite::Database* database::connectionRO() +{ + return global().connectionRO(); +} + +// ----------------------------------------------------------------------------- +// Returns the read+write connection to the database (for the calling thread) +// ----------------------------------------------------------------------------- +SQLite::Database* database::connectionRW() +{ + return global().connectionRW(); +} + +// ----------------------------------------------------------------------------- +// Returns true if the program database file exists +// ----------------------------------------------------------------------------- +bool database::fileExists() +{ + return fileutil::fileExists(app::path("slade.sqlite", app::Dir::User)); +} + +// ----------------------------------------------------------------------------- +// Returns true if a transaction (BEGIN -> COMMIT/ROLLBACK) is currently active +// on [connection] +// ----------------------------------------------------------------------------- +bool database::isTransactionActive(const SQLite::Database* connection) +{ + return sqlite3_get_autocommit(connection->getHandle()) == 0; +} + +// ----------------------------------------------------------------------------- +// Returns the path to the program database file +// ----------------------------------------------------------------------------- +string database::programDatabasePath() +{ + return app::path("slade.sqlite", app::Dir::User); +} + +// ----------------------------------------------------------------------------- +// Returns the current database session id +// ----------------------------------------------------------------------------- +int64_t database::sessionId() +{ + return session_id; +} + +// ----------------------------------------------------------------------------- +// Initialises the program database, creating it if it doesn't exist and opening +// the 'global' connection context. +// Returns false if the database couldn't be created or the global context +// failed to open, true otherwise +// ----------------------------------------------------------------------------- +bool database::init() +{ + auto db_path = app::path("slade.sqlite", app::Dir::User); + + // Create database if needed + bool created = false; + if (!fileutil::fileExists(db_path)) + { + if (!createDatabase(db_path)) + return false; + + created = true; + } + + // Open global connections to database (for main thread usage only) + if (!global().open(db_path)) + { + global::error = "Unable to open global database connections"; + return false; + } + + // Migrate pre-3.3.0 config stuff to database + if (created) + migrateConfigs(); + + // Update the database if needed + auto existing_version = connectionRO()->execAndGet("SELECT version FROM db_info").getInt(); + if (existing_version < db_version) + return updateDatabase(existing_version); + + // Write new session + if (!beginSession()) + return false; + + return true; +} + +// ----------------------------------------------------------------------------- +// Closes the global connection context to the database +// ----------------------------------------------------------------------------- +void database::close() +{ + endSession(); + global().vacuum(); // Shrink size on disk + global().close(); +} + +// ----------------------------------------------------------------------------- +// Migrates various configurations from text/cfg files (pre-3.3.0) to the +// SLADE program database +// ----------------------------------------------------------------------------- +void database::migrateConfigs() +{ +#define MIGRATE_CVAR_BOOL(cvar, state) else if (tz.check(#cvar)) ui::saveStateBool(#state, tz.peek().asBool()) +#define MIGRATE_CVAR_INT(cvar, state) else if (tz.check(#cvar)) ui::saveStateInt(#state, tz.peek().asInt()) +#define MIGRATE_CVAR_STRING(cvar, state) else if (tz.check(#cvar)) ui::saveStateString(#state, tz.peek().text) + + // Migrate window layouts from .layout files + migrateWindowLayout("mainwindow.layout", "main"); + migrateWindowLayout("mapwindow.layout", "map"); + migrateWindowLayout("scriptmanager.layout", "scriptmanager"); + + // Migrate various things from SLADE.cfg + Tokenizer tz; + if (!tz.openFile(app::path("slade3.cfg", app::Dir::User))) + return; + while (!tz.atEnd()) + { + // Migrate old CVars to UI state table + if (tz.advIf("cvars", 2)) + { + while (!tz.checkOrEnd("}")) + { + // Last archive format + if (tz.check("archive_last_created_format")) + ui::saveStateString("ArchiveLastCreatedFormat", tz.peek().text); + + // Window maximized flags + MIGRATE_CVAR_BOOL(browser_maximised, BrowserWindowMaximized); + MIGRATE_CVAR_BOOL(mw_maximized, MainWindowMaximized); + MIGRATE_CVAR_BOOL(mew_maximized, MapEditorWindowMaximized); + MIGRATE_CVAR_BOOL(sm_maximized, ScriptManagerWindowMaximized); + + // Entry list column widths + MIGRATE_CVAR_INT(elist_colsize_index, EntryListIndexWidth); + MIGRATE_CVAR_INT(elist_colsize_size, EntryListSizeWidth); + MIGRATE_CVAR_INT(elist_colsize_type, EntryListTypeWidth); + MIGRATE_CVAR_INT(elist_colsize_name_list, EntryListNameWidthList); + MIGRATE_CVAR_INT(elist_colsize_name_tree, EntryListNameWidthTree); + + // Entry list column visibility + MIGRATE_CVAR_BOOL(elist_colindex_show, EntryListIndexVisible); + MIGRATE_CVAR_BOOL(elist_colsize_show, EntryListSizeVisible); + MIGRATE_CVAR_BOOL(elist_coltype_show, EntryListTypeVisible); + + // Splitter position + MIGRATE_CVAR_INT(ap_splitter_position_list, ArchivePanelSplitPosList); + MIGRATE_CVAR_INT(ap_splitter_position_tree, ArchivePanelSplitPosTree); + + // Colourize/Tint Dialogs + MIGRATE_CVAR_STRING(last_colour, ColouriseDialogLastColour); + MIGRATE_CVAR_STRING(last_tint_colour, TintDialogLastColour); + MIGRATE_CVAR_INT(last_tint_amount, TintDialogLastAmount); + + // Zoom sliders + MIGRATE_CVAR_INT(zoom_gfx, ZoomGfxCanvas); + MIGRATE_CVAR_INT(zoom_ctex, ZoomCTextureCanvas); + + // Misc. + MIGRATE_CVAR_BOOL(setup_wizard_run, SetupWizardRun); + + tz.adv(2); + } + + tz.adv(); // Skip ending } + } + + // Migrate window size/position info + if (tz.advIf("window_info", 2)) + { + tz.advIf("{"); + while (!tz.check("}") && !tz.atEnd()) + { + auto id = tz.current().text; + int width = tz.next().asInt(); + int height = tz.next().asInt(); + int left = tz.next().asInt(); + int top = tz.next().asInt(); + ui::setWindowInfo(id.c_str(), width, height, left, top); + tz.adv(); + } + } + + // Next token + tz.adv(); + } +#undef MIGRATE_CVAR_BOOL +#undef MIGRATE_CVAR_INT +#undef MIGRATE_CVAR_STRING +} + + +// ----------------------------------------------------------------------------- +// +// Console Commands +// +// ----------------------------------------------------------------------------- + + +void c_db(const vector& args); +ConsoleCommand db_cmd("db", &c_db, 1, false); +void c_db(const vector& args) +{ + const auto& command = args[0]; + + try + { + // List tables + if (command == "tables") + { + if (auto db = database::connectionRO()) + { + SQLite::Statement sql(*db, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name"); + while (sql.executeStep()) + log::console(sql.getColumn(0).getString()); + } + } + + // Row count of table + else if (command == "rowcount") + { + if (args.size() < 2) + { + log::console("No table name given. Usage: db rowcount "); + return; + } + + if (auto db = database::connectionRO()) + { + SQLite::Statement sql(*db, fmt::format("SELECT COUNT(*) FROM {}", args[1])); + if (sql.executeStep()) + log::console("{} rows", sql.getColumn(0).getInt()); + else + log::console("No such table"); + } + } + + // Reset table from template + else if (command == "reset") + { + if (args.size() < 2) + { + log::console("No table name given. Usage: db reset "); + return; + } + + const auto& table = args[1]; + + if (auto db = database::connectionRW()) + { + auto sql_entry = app::programResource()->entryAtPath(fmt::format("database/tables/{}.sql", table)); + if (!sql_entry) + { + log::console("Can't find table sql script for {}", table); + return; + } + + string sql{ reinterpret_cast(sql_entry->data().data()), sql_entry->data().size() }; + db->exec(fmt::format("DROP TABLE IF EXISTS {}", table)); + db->exec(sql); + log::console("Table {} recreated and reset to default", table); + } + } + } + catch (const SQLite::Exception& ex) + { + log::error(ex.what()); + } +} diff --git a/src/Database/Database.h b/src/Database/Database.h new file mode 100644 index 000000000..9ccca2502 --- /dev/null +++ b/src/Database/Database.h @@ -0,0 +1,33 @@ +#pragma once + +namespace SQLite +{ +class Database; +} + +namespace slade::database +{ +class Statement; +class Transaction; + +// Helpers +bool isTransactionActive(const SQLite::Database* connection); +int exec(const string& query, SQLite::Database* connection = nullptr); +int exec(const char* query, SQLite::Database* connection = nullptr); +bool viewExists(string_view view_name, const SQLite::Database& connection); +bool rowIdExists(string_view table_name, int64_t id, string_view id_col = "id"); +Statement* cacheQuery(string_view id, string_view sql, bool writes = false); +SQLite::Database* connectionRO(); +SQLite::Database* connectionRW(); + +// General +bool fileExists(); +string programDatabasePath(); +int64_t sessionId(); +bool init(); +void close(); +void migrateConfigs(); +} // namespace slade::database + +// Shortcut alias for database namespace +namespace db = slade::database; diff --git a/src/Database/DbUtils.h b/src/Database/DbUtils.h new file mode 100644 index 000000000..a33282ddf --- /dev/null +++ b/src/Database/DbUtils.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Statement.h" + +namespace slade::database +{ +template bool rowExists(SQLite::Database& connection, string_view table_name, string_view col_name, T value) +{ + Statement sql(connection, fmt::format("SELECT * FROM {} WHERE {} = ?", table_name, col_name)); + sql.bind(1, value); + return sql.executeStep(); +} +} // namespace slade::database diff --git a/src/Database/Statement.h b/src/Database/Statement.h new file mode 100644 index 000000000..2cbf546fa --- /dev/null +++ b/src/Database/Statement.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace slade::database +{ +// Wrapper of SQLiteCpp Statement to handle string_view +class Statement : public SQLite::Statement +{ +public: + Statement(const SQLite::Database& db, string_view query) : + SQLite::Statement(db, string{ query.data(), query.size() }) + { + } + + void bind(int index, int32_t value) { SQLite::Statement::bind(index, value); } + void bind(int index, uint32_t value) { SQLite::Statement::bind(index, value); } + void bind(int index, int64_t value) { SQLite::Statement::bind(index, value); } + void bind(int index, double value) { SQLite::Statement::bind(index, value); } + void bind(int index, const string& value) { SQLite::Statement::bind(index, value); } + void bind(int index, const char* value) { SQLite::Statement::bind(index, value); } + void bind(int index, string_view value) { SQLite::Statement::bind(index, string{ value.data(), value.size() }); } + void bind(int index, const void* value, int size) { SQLite::Statement::bind(index, value, size); } + void bind(int index) { SQLite::Statement::bind(index); } + + // Needed to avoid ambiguous call error on some systems for time_t + void bindDateTime(int index, time_t value) { SQLite::Statement::bind(index, static_cast(value)); } + + void bindNoCopy(int index, const string& value) { SQLite::Statement::bindNoCopy(index, value); } + void bindNoCopy(int index, const char* value) { SQLite::Statement::bindNoCopy(index, value); } + void bindNoCopy(int index, string_view value) + { + SQLite::Statement::bindNoCopy(index, string{ value.data(), value.size() }); + } + void bindNoCopy(int index, const void* value, int size) { SQLite::Statement::bindNoCopy(index, value, size); } +}; +} // namespace slade::database diff --git a/src/Database/Transaction.cpp b/src/Database/Transaction.cpp new file mode 100644 index 000000000..f2cd8e13e --- /dev/null +++ b/src/Database/Transaction.cpp @@ -0,0 +1,123 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: Transaction.cpp +// Description: Encapsulates a single SQL transaction, ensuring it's closed off +// properly etc. via RAII +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "Transaction.h" +#include "Database.h" +#include + +using namespace slade; +using namespace database; + + +// ----------------------------------------------------------------------------- +// +// Transaction Class Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Transaction class constructor +// ----------------------------------------------------------------------------- +Transaction::Transaction(SQLite::Database* connection, bool begin) : connection_{ connection }, has_begun_{ begin } +{ + if (begin) + connection->exec("BEGIN"); +} + +// ----------------------------------------------------------------------------- +// Transaction class move constructor +// ----------------------------------------------------------------------------- +Transaction::Transaction(Transaction&& other) noexcept : + connection_{ other.connection_ }, + has_begun_{ other.has_begun_ }, + has_ended_{ other.has_ended_ } +{ + other.connection_ = nullptr; + other.has_begun_ = true; + other.has_ended_ = true; +} + +// ----------------------------------------------------------------------------- +// Transaction class destructor +// ----------------------------------------------------------------------------- +Transaction::~Transaction() +{ + if (has_begun_ && !has_ended_) + connection_->exec("ROLLBACK"); +} + +// ----------------------------------------------------------------------------- +// Begins the transaction if it isn't active already +// ----------------------------------------------------------------------------- +void Transaction::begin() +{ + if (has_begun_) + return; + + connection_->exec("BEGIN"); + has_begun_ = true; +} + +// ----------------------------------------------------------------------------- +// Begins the transaction if there are no currently active transactions on the +// connection +// ----------------------------------------------------------------------------- +void Transaction::beginIfNoActiveTransaction() +{ + if (!isTransactionActive(connection_)) + begin(); +} + +// ----------------------------------------------------------------------------- +// Commits the transaction +// ----------------------------------------------------------------------------- +void Transaction::commit() +{ + if (!has_begun_ || has_ended_) + return; + + connection_->exec("COMMIT"); + has_ended_ = true; +} + +// ----------------------------------------------------------------------------- +// Rolls the transaction back +// ----------------------------------------------------------------------------- +void Transaction::rollback() +{ + if (!has_begun_ || has_ended_) + return; + + connection_->exec("ROLLBACK"); + has_ended_ = true; +} diff --git a/src/Database/Transaction.h b/src/Database/Transaction.h new file mode 100644 index 000000000..e65a63e6c --- /dev/null +++ b/src/Database/Transaction.h @@ -0,0 +1,30 @@ +#pragma once + +namespace SQLite +{ +class Database; +} + +namespace slade::database +{ +class Transaction +{ +public: + Transaction(SQLite::Database* connection, bool begin = true); + Transaction(Transaction&& other) noexcept; + ~Transaction(); + + Transaction(const Transaction&) = delete; + Transaction& operator=(const Transaction&) = delete; + + void begin(); + void beginIfNoActiveTransaction(); + void commit(); + void rollback(); + +private: + SQLite::Database* connection_ = nullptr; + bool has_begun_ = false; + bool has_ended_ = false; +}; +} // namespace slade::database diff --git a/src/Game/Game.cpp b/src/Game/Game.cpp index b40d92fdc..b40fb87c1 100644 --- a/src/Game/Game.cpp +++ b/src/Game/Game.cpp @@ -419,7 +419,7 @@ void game::init() [=]() { Archive zdoom_pk3(ArchiveFormat::Zip); - if (!zdoom_pk3.open(zdoom_pk3_path)) + if (!zdoom_pk3.open(zdoom_pk3_path, true)) return; // ZScript diff --git a/src/General/CVar.cpp b/src/General/CVar.cpp index 3f00dcb34..04b52cda4 100644 --- a/src/General/CVar.cpp +++ b/src/General/CVar.cpp @@ -199,7 +199,7 @@ string CVar::writeAll() } } - format_to(buf, "}}\n\n"); + format_to(buf, "}}\n"); return to_string(mem_buf); } diff --git a/src/General/Log.h b/src/General/Log.h index 83eb4a159..97b60b933 100644 --- a/src/General/Log.h +++ b/src/General/Log.h @@ -112,6 +112,15 @@ template void error(string_view text, const Args&... args) message(MessageType::Error, text, fmt::make_format_args(args...)); } +template void console(int level, string_view text, const Args&... args) +{ + message(MessageType::Console, level, text, fmt::make_format_args(args...)); +} +template void console(string_view text, const Args&... args) +{ + message(MessageType::Console, text, fmt::make_format_args(args...)); +} + template void debug(int level, string_view text, const Args&... args) { if (global::debug) diff --git a/src/General/MapPreviewData.cpp b/src/General/MapPreviewData.cpp index 6a4fbbb6c..70fe8d97e 100644 --- a/src/General/MapPreviewData.cpp +++ b/src/General/MapPreviewData.cpp @@ -344,7 +344,7 @@ bool MapPreviewData::openMap(MapDesc map) // Attempt to open entry as wad archive temp_archive = std::make_unique(ArchiveFormat::Wad); - if (!temp_archive->open(m_head->data())) + if (!temp_archive->open(m_head->data(), true)) { return false; } diff --git a/src/General/Misc.cpp b/src/General/Misc.cpp index 1c2980127..a5ac46546 100644 --- a/src/General/Misc.cpp +++ b/src/General/Misc.cpp @@ -40,7 +40,6 @@ #include "Graphics/SImage/SImage.h" #include "Utility/PropertyList.h" #include "Utility/StringUtils.h" -#include "Utility/Tokenizer.h" using namespace slade; @@ -54,10 +53,6 @@ CVAR(Bool, size_as_string, true, CVar::Flag::Save) CVAR(Bool, percent_encoding, false, CVar::Flag::Save) EXTERN_CVAR(Float, col_cie_tristim_x) EXTERN_CVAR(Float, col_cie_tristim_z) -namespace slade::misc -{ -vector window_info; -} // ----------------------------------------------------------------------------- @@ -552,72 +547,3 @@ Vec2i misc::findJaguarTextureDimensions(const ArchiveEntry* entry, string_view n // We didn't find the texture return dimensions; } - -// ----------------------------------------------------------------------------- -// Gets the saved window info for [id] -// ----------------------------------------------------------------------------- -misc::WindowInfo misc::getWindowInfo(string_view id) -{ - for (auto& a : window_info) - { - if (a.id == id) - return a; - } - - return WindowInfo("", -1, -1, -1, -1); -} - -// ----------------------------------------------------------------------------- -// Sets the saved window info for [id] -// ----------------------------------------------------------------------------- -void misc::setWindowInfo(string_view id, int width, int height, int left, int top) -{ - if (id.empty()) - return; - - for (auto& a : window_info) - { - if (a.id == id) - { - if (width >= -1) - a.width = width; - if (height >= -1) - a.height = height; - if (left >= -1) - a.left = left; - if (top >= -1) - a.top = top; - return; - } - } - - window_info.emplace_back(id, width, height, left, top); -} - -// ----------------------------------------------------------------------------- -// Reads saved window info from tokenizer [tz] -// ----------------------------------------------------------------------------- -void misc::readWindowInfo(Tokenizer& tz) -{ - // Read definitions - tz.advIf("{"); - while (!tz.check("}") && !tz.atEnd()) - { - auto id = tz.current().text; - int width = tz.next().asInt(); - int height = tz.next().asInt(); - int left = tz.next().asInt(); - int top = tz.next().asInt(); - setWindowInfo(id, width, height, left, top); - tz.adv(); - } -} - -// ----------------------------------------------------------------------------- -// Writes all saved window info to [file] -// ----------------------------------------------------------------------------- -void misc::writeWindowInfo(wxFile& file) -{ - for (auto& a : window_info) - file.Write(wxString::Format("\t%s %d %d %d %d\n", a.id, a.width, a.height, a.left, a.top)); -} diff --git a/src/General/Misc.h b/src/General/Misc.h index 9b260eba9..6d2aaea0f 100644 --- a/src/General/Misc.h +++ b/src/General/Misc.h @@ -36,19 +36,5 @@ namespace misc // Mass Rename string massRenameFilter(const vector& names); void doMassRename(vector& names, string_view name_filter); - - // Dialog/Window sizes - struct WindowInfo - { - string id; - int width, height, left, top; - WindowInfo(string_view id, int w, int h, int l, int t) : id{ id }, width{ w }, height{ h }, left{ l }, top{ t } - { - } - }; - WindowInfo getWindowInfo(string_view id); - void setWindowInfo(string_view id, int width, int height, int left, int top); - void readWindowInfo(Tokenizer& tz); - void writeWindowInfo(wxFile& file); } // namespace misc } // namespace slade diff --git a/src/General/ResourceManager.cpp b/src/General/ResourceManager.cpp index 021a689a8..b12d71a7b 100644 --- a/src/General/ResourceManager.cpp +++ b/src/General/ResourceManager.cpp @@ -356,7 +356,7 @@ void ResourceManager::addEntry(shared_ptr& entry) strutil::upperIP(path); path.erase(0, 1); - log::debug("Adding entry {} to resource manager", path); + //log::debug("Adding entry {} to resource manager", path); // Check for palette entry if (type->id() == "palette") @@ -483,7 +483,7 @@ void ResourceManager::removeEntry(ArchiveEntry* entry, string_view entry_name, b strutil::upperIP(path); path.erase(0, 1); - log::debug("Removing entry {} from resource manager", path); + //log::debug("Removing entry {} from resource manager", path); // Remove from palettes removeEntryFromMap(palettes_, name, entry, full_check); diff --git a/src/General/UI.cpp b/src/General/UI.cpp index 2f9c25d99..d394abe9b 100644 --- a/src/General/UI.cpp +++ b/src/General/UI.cpp @@ -32,9 +32,14 @@ #include "Main.h" #include "UI.h" #include "App.h" +#include "Database/Context.h" +#include "Database/Transaction.h" #include "General/Console.h" #include "UI/SplashWindow.h" +#include "UI/State.h" #include "Utility/StringUtils.h" +#include +#include using namespace slade; @@ -77,7 +82,8 @@ bool isMainThread() } // namespace slade::ui // ----------------------------------------------------------------------------- -// Initialises UI metric values based on [scale] +// Initialises UI metric values based on [scale] and other various UI related +// things // ----------------------------------------------------------------------------- void ui::init(double scale) { @@ -87,6 +93,7 @@ void ui::init(double scale) scale = splash_window->GetDPIScaleFactor(); #endif + // Set scale + metrics ui::scale = scale; px_pad_small = 8 * scale; px_pad = 12 * scale; @@ -98,6 +105,9 @@ void ui::init(double scale) px_spin_width = 64 * scale; SplashWindow::init(); + + // Init saved state props + initStateProps(); } // ----------------------------------------------------------------------------- @@ -277,6 +287,126 @@ int ui::padMin() return px_pad_min; } +// ----------------------------------------------------------------------------- +// Returns the saved window info for window/dialog [id] +// ----------------------------------------------------------------------------- +ui::WindowInfo ui::getWindowInfo(const char* id) +{ + WindowInfo inf{ {}, 0, 0, 0, 0 }; + + try + { + if (auto* sql = database::global().cacheQuery( + "get_window_info", "SELECT left, top, width, height FROM window_info WHERE window_id = ?")) + { + sql->bind(1, id); + if (sql->executeStep()) + { + inf.id = id; + inf.left = sql->getColumn(0).getInt(); + inf.top = sql->getColumn(1).getInt(); + inf.width = sql->getColumn(2).getInt(); + inf.height = sql->getColumn(3).getInt(); + } + + sql->reset(); + } + } + catch (const SQLite::Exception& ex) + { + log::error("Error getting window info for \"{}\": {}", id, ex.what()); + } + + return inf; +} + +// ----------------------------------------------------------------------------- +// Saves the window info for window/dialog [id] +// ----------------------------------------------------------------------------- +void ui::setWindowInfo(const char* id, int width, int height, int left, int top) +{ + try + { + if (auto sql = database::global().cacheQuery( + "set_window_info", + "REPLACE INTO window_info (window_id, left, top, width, height) " + "VALUES (?,?,?,?,?)", + true)) + { + sql->clearBindings(); + sql->bind(1, id); + sql->bind(2, left); + sql->bind(3, top); + sql->bind(4, width); + sql->bind(5, height); + + sql->exec(); + sql->reset(); + } + } + catch (const SQLite::Exception& ex) + { + log::error("Error writing window info for \"{}\": {}", id, ex.what()); + } +} + +// ----------------------------------------------------------------------------- +// Returns the saved window AUI layout for window [id] +// ----------------------------------------------------------------------------- +vector ui::getWindowLayout(const char* id) +{ + vector layout; + + try + { + if (auto sql = database::global().cacheQuery( + "get_window_layout", "SELECT component, layout FROM window_layout WHERE window_id = ?")) + { + sql->bind(1, id); + while (sql->executeStep()) + layout.emplace_back(sql->getColumn(0).getString(), sql->getColumn(1).getString()); + sql->reset(); + } + } + catch (const SQLite::Exception& ex) + { + log::error("Error getting window layout for \"{}\": {}", id, ex.what()); + } + + return layout; +} + +// ----------------------------------------------------------------------------- +// Saves the AUI layout for window [id] +// ----------------------------------------------------------------------------- +void ui::setWindowLayout(const char* id, const vector& layout) +{ + try + { + auto transaction = database::global().beginTransaction(true); + + if (auto sql = database::global().cacheQuery( + "set_window_layout", "REPLACE INTO window_layout VALUES (?, ?, ?)", true)) + { + sql->clearBindings(); + sql->bind(1, id); + for (const auto& row : layout) + { + sql->bind(2, row.first); + sql->bind(3, row.second); + sql->exec(); + sql->reset(); + } + } + + transaction.commit(); + } + catch (const SQLite::Exception& ex) + { + log::error("Error writing window layout for \"{}\": {}", id, ex.what()); + } +} + // ----------------------------------------------------------------------------- // diff --git a/src/General/UI.h b/src/General/UI.h index a1ddd7a5c..bf3192ae7 100644 --- a/src/General/UI.h +++ b/src/General/UI.h @@ -1,5 +1,7 @@ #pragma once +#include "Utility/StringPair.h" + namespace slade::ui { // General @@ -47,4 +49,15 @@ int pad(); // Shortcut for ui::px(UI::Size::Pad) int padLarge(); // Shortcut for ui::px(UI::Size::PadLarge) int padMin(); // Shortcut for ui::px(ui::Size::PadMinimum) +// Window size, position & layout persistence +struct WindowInfo +{ + string id; + int left, top, width, height; +}; +WindowInfo getWindowInfo(const char* id); +void setWindowInfo(const char* id, int width, int height, int left, int top); +vector getWindowLayout(const char* id); +void setWindowLayout(const char* id, const vector& layout); + } // namespace slade::ui diff --git a/src/Graphics/Icons.cpp b/src/Graphics/Icons.cpp index 88f6b74f4..384a40d87 100644 --- a/src/Graphics/Icons.cpp +++ b/src/Graphics/Icons.cpp @@ -576,7 +576,7 @@ wxBitmap icons::getInterfaceIcon(string_view name, int size, InterfaceTheme them } // Get icon definition - auto& icon_set = dark ? iconset_ui_dark : iconset_ui_light; + auto& icon_set = dark ? iconset_ui_dark : iconset_ui_light; IconDef* icon_def = nullptr; if (auto i = icon_set.icons.find(name); i != icon_set.icons.end()) icon_def = &i->second; @@ -623,3 +623,26 @@ bool icons::iconExists(Type type, string_view name) { return iconDef(type, name) != nullptr; } + + +// ----------------------------------------------------------------------------- +// +// IconCache Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Adds an icon to the cache +// ----------------------------------------------------------------------------- +void icons::IconCache::cacheIcon(Type type, const string& name, int size, Point2i padding) +{ +#if wxCHECK_VERSION(3, 1, 6) + const auto bundle = getIcon(type, name, size, padding); + icons[name] = bundle; +#else + const auto bmp = icons::getIcon(type, name, ui::scalePx(size), padding); + wxIcon icon; + icon.CopyFromBitmap(bmp); + icons[name] = icon; +#endif +} diff --git a/src/Graphics/Icons.h b/src/Graphics/Icons.h index a99e35a19..487410653 100644 --- a/src/Graphics/Icons.h +++ b/src/Graphics/Icons.h @@ -1,5 +1,7 @@ #pragma once +#include + namespace slade { namespace icons @@ -31,5 +33,17 @@ namespace icons vector iconSets(Type type); bool iconExists(Type type, string_view name); + + struct IconCache + { +#if wxCHECK_VERSION(3, 1, 6) + std::unordered_map icons; +#else + std::unordered_map icons; +#endif + + bool isCached(const string& name) { return icons.find(name) != icons.end(); } + void cacheIcon(Type type, const string& name, int size, Point2i padding = {}); + }; } // namespace icons } // namespace slade diff --git a/src/Library/ArchiveEntry.cpp b/src/Library/ArchiveEntry.cpp new file mode 100644 index 000000000..e3400859e --- /dev/null +++ b/src/Library/ArchiveEntry.cpp @@ -0,0 +1,681 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: ArchiveEntry.cpp +// Description: ArchiveEntryRow struct and related functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "Archive/ArchiveEntry.h" +#include "App.h" +#include "Archive/EntryType/EntryType.h" +#include "ArchiveEntry.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include "Database/Transaction.h" +#include +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +string update_archive_entry = + "UPDATE archive_entry " + "SET path = ?, [index] = ?, name = ?, size = ?, hash = ?, type_id = ? " + "WHERE archive_id = ? AND id = ?"; +string insert_archive_entry = + "INSERT INTO archive_entry (archive_id, id, path, [index], name, size, hash, type_id) " + "VALUES (?,?,?,?,?,?,?,?)"; +string copy_archive_entries = + "INSERT INTO archive_entry (archive_id, id, path, [index], name, size, hash, type_id) " + " SELECT ?, id, path, [index], name, size, hash, type_id " + " FROM archive_entry WHERE archive_id = ?"; +string insert_archive_entry_property = "INSERT INTO archive_entry_property VALUES (?,?,?,?,?)"; +string get_archive_entry_properties = + "SELECT key, value_type, value FROM archive_entry_property WHERE archive_id = ? AND entry_id = ?"; + +vector saved_ex_props = { "TextPosition", "TextLanguage" }; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// ArchiveEntryRow Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// ArchiveEntryRow constructor +// Initializes the row for [archive_id]+[id] with info from [entry] +// ----------------------------------------------------------------------------- +ArchiveEntryRow::ArchiveEntryRow(int64_t archive_id, int64_t id, const ArchiveEntry& entry) : + archive_id{ archive_id }, + id{ id }, + path{ entry.path() }, + index{ entry.index() }, + name{ entry.name() }, + size{ entry.size() }, + hash{ entry.hash() }, + type_id{ entry.type()->id() } +{ +} + +// ----------------------------------------------------------------------------- +// ArchiveEntryRow constructor +// Reads existing data from the database. If row [archive_id]+[id] doesn't exist +// in the database, the row id will be set to -1 +// ----------------------------------------------------------------------------- +ArchiveEntryRow::ArchiveEntryRow(db::Context& db, int64_t archive_id, int64_t id) : archive_id{ archive_id }, id{ id } +{ + if (auto sql = db.cacheQuery("get_archive_entry", "SELECT * FROM archive_entry WHERE archive_id = ? AND id = ?")) + { + sql->clearBindings(); + sql->bind(1, archive_id); + sql->bind(2, id); + + if (sql->executeStep()) + { + path = sql->getColumn(2).getString(); + index = sql->getColumn(3).getInt(); + name = sql->getColumn(4).getString(); + size = sql->getColumn(5).getUInt(); + hash = sql->getColumn(6).getString(); + type_id = sql->getColumn(7).getString(); + } + else + { + log::warning("archive_entry row with archive_id {}, id {} does not exist in the database", archive_id, id); + this->id = -1; + } + + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Inserts this row into the database. +// If successful, the inserted row id is returned, otherwise returns -1. +// (note the returned row id isn't the 'id' column since that is not a primary +// key, it returns the sqlite rowid of the row) +// ----------------------------------------------------------------------------- +int64_t ArchiveEntryRow::insert() const +{ + if (id >= 0) + { + log::warning("Trying to insert archive_entry id {} that already exists", id); + return id; + } + + int64_t row_id = -1; + if (auto sql = db::cacheQuery("insert_archive_entry", insert_archive_entry, true)) + { + sql->clearBindings(); + + sql->bind(1, archive_id); + sql->bind(2, id); + sql->bind(3, path); + sql->bind(4, index); + sql->bind(5, name); + sql->bind(6, size); + sql->bind(7, hash); + sql->bind(8, type_id); + + if (sql->exec() > 0) + row_id = db::connectionRW()->getLastInsertRowid(); + + sql->reset(); + } + + return row_id; +} + +// ----------------------------------------------------------------------------- +// Updates this row in the database +// ----------------------------------------------------------------------------- +bool ArchiveEntryRow::update() const +{ + // Ignore invalid id + if (id < 0) + { + log::warning("Trying to update archive_entry row with no id"); + return false; + } + + auto rows = 0; + if (auto sql = db::cacheQuery("update_archive_entry", update_archive_entry, true)) + { + sql->clearBindings(); + + sql->bind(1, path); + sql->bind(2, index); + sql->bind(3, name); + sql->bind(4, size); + sql->bind(5, hash); + sql->bind(6, type_id); + sql->bind(7, archive_id); + sql->bind(8, id); + + rows = sql->exec(); + sql->reset(); + } + + return rows > 0; +} + +// ----------------------------------------------------------------------------- +// Removes this row from the database. +// If successful, id will be set to -1 +// ----------------------------------------------------------------------------- +bool ArchiveEntryRow::remove() +{ + // Ignore invalid id + if (id < 0) + { + log::warning("Trying to remove archive_entry row with no id"); + return false; + } + + auto rows = 0; + + if (auto sql = db::cacheQuery("delete_archive_entry", "DELETE FROM archive_entry WHERE archive_id = ? AND id = ?")) + { + sql->bind(1, archive_id); + sql->bind(2, id); + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + id = -1; + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +// ----------------------------------------------------------------------------- +// Returns a list of all ArchiveEntryRows for [archive_id] +// ----------------------------------------------------------------------------- +vector getArchiveEntryRows(int64_t archive_id) +{ + vector rows; + + if (auto sql = db::cacheQuery( + "get_archive_entries_for_archive", "SELECT * FROM archive_entry WHERE archive_id = ?")) + { + sql->clearBindings(); + sql->bind(1, archive_id); + + while (sql->executeStep()) + { + rows.emplace_back( + archive_id, + sql->getColumn(1).getInt64(), + sql->getColumn(2).getString(), + sql->getColumn(3).getInt(), + sql->getColumn(4).getString(), + sql->getColumn(5).getUInt(), + sql->getColumn(6).getString(), + sql->getColumn(7).getString()); + } + + sql->reset(); + } + + return rows; +} + +// ----------------------------------------------------------------------------- +// Mass updates the given [rows] in the database +// ----------------------------------------------------------------------------- +void updateArchiveEntryRows(const vector& rows) +{ + if (auto sql = db::cacheQuery("update_archive_entry", update_archive_entry, true)) + { + // Begin transaction if none currently active + db::Transaction transaction(db::connectionRW(), false); + transaction.beginIfNoActiveTransaction(); + + for (const auto& row : rows) + { + sql->clearBindings(); + + sql->bind(1, row.path); + sql->bind(2, row.index); + sql->bind(3, row.name); + sql->bind(4, row.size); + sql->bind(5, row.hash); + sql->bind(6, row.type_id); + sql->bind(7, row.archive_id); + sql->bind(8, row.id); + + sql->exec(); + + sql->reset(); + } + + transaction.commit(); + } +} + +// ----------------------------------------------------------------------------- +// Mass inserts the given [rows] in the database +// ----------------------------------------------------------------------------- +void insertArchiveEntryRows(const vector& rows) +{ + if (auto sql = db::cacheQuery("insert_archive_entry", insert_archive_entry, true)) + { + // Begin transaction if none currently active + db::Transaction transaction(db::connectionRW(), false); + transaction.beginIfNoActiveTransaction(); + + for (auto& row : rows) + { + sql->clearBindings(); + + sql->bind(1, row.archive_id); + sql->bind(2, row.id); + sql->bind(3, row.path); + sql->bind(4, row.index); + sql->bind(5, row.name); + sql->bind(6, row.size); + sql->bind(7, row.hash); + sql->bind(8, row.type_id); + + sql->exec(); + + sql->reset(); + } + + transaction.commit(); + } +} + +// ----------------------------------------------------------------------------- +// Mass deletes all archive_entry rows for [archive_id] in the database +// ----------------------------------------------------------------------------- +int deleteArchiveEntryRowsByArchiveId(int64_t archive_id) +{ + int rows = 0; + + if (auto sql = db::cacheQuery( + "delete_archive_entry_by_archive", "DELETE FROM archive_entry WHERE archive_id = ?", true)) + { + sql->clearBindings(); + sql->bind(1, archive_id); + rows = sql->exec(); + sql->reset(); + } + + return rows; +} + +// ----------------------------------------------------------------------------- +// Returns true if [row] exactly matches the given entry details. +// +// Note this currently ignores index so that if an entry has been moved up/down +// externally it will still be able to be matched (it's very likely to be the +// same entry) +// ----------------------------------------------------------------------------- +inline bool entryRowExactMatch( + const ArchiveEntryRow& row, + string_view entry_path, + string_view entry_name, + unsigned entry_size, + string_view entry_hash) +{ + return row.path == entry_path && row.name == entry_name && row.size == entry_size && row.hash == entry_hash; +} + +void writeEntryProperties(SQLite::Statement* sql, int64_t archive_id, const ArchiveEntry* entry) +{ + for (const auto& prop : entry->exProps().properties()) + { + if (VECTOR_EXISTS(saved_ex_props, prop.name)) + { + sql->bind(1, archive_id); + sql->bind(2, entry->libraryId()); + sql->bind(3, prop.name); + sql->bind(4, static_cast(prop.value.index())); + switch (prop.value.index()) + { + case 0: sql->bind(5, std::get(prop.value)); break; + case 1: sql->bind(5, std::get(prop.value)); break; + case 2: sql->bind(5, std::get(prop.value)); break; + case 3: sql->bind(5, std::get(prop.value)); break; + case 4: sql->bind(5, std::get(prop.value)); break; + default: sql->bind(5, 0); break; // Shouldn't happen + } + sql->exec(); + sql->reset(); + } + } +} + +void readEntryProperties(int64_t archive_id, ArchiveEntry* entry) +{ + if (auto sql = db::cacheQuery("get_archive_entry_properties", get_archive_entry_properties)) + { + sql->bind(1, archive_id); + sql->bind(2, entry->libraryId()); + + while (sql->executeStep()) + { + auto key = sql->getColumn(0).getString(); + switch (sql->getColumn(1).getInt()) + { + case 0: entry->exProp(key) = sql->getColumn(2).getInt() > 0; break; + case 1: entry->exProp(key) = sql->getColumn(2).getInt(); break; + case 2: entry->exProp(key) = sql->getColumn(2).getUInt(); break; + case 3: entry->exProp(key) = sql->getColumn(2).getDouble(); break; + case 4: entry->exProp(key) = sql->getColumn(2).getString(); break; + default: break; // Shouldn't happen + } + } + + sql->reset(); + } +} +} // namespace slade::library + +// ----------------------------------------------------------------------------- +// Copies all archive_entry rows from [from_archive_id] and inserts them for +// [to_archive_id] +// ----------------------------------------------------------------------------- +int library::copyArchiveEntries(int64_t from_archive_id, int64_t to_archive_id) +{ + // Check args + if (from_archive_id < 0 || to_archive_id < 0) + return 0; + + int n_copied = 0; + + if (auto sql = db::cacheQuery("copy_archive_entries", copy_archive_entries, true)) + { + sql->bind(1, to_archive_id); + sql->bind(2, from_archive_id); + + n_copied = sql->exec(); + + sql->reset(); + } + + return n_copied; +} + +// ----------------------------------------------------------------------------- +// Rebuilds all archive_entry rows for [archive_id] using the given [entries] +// ----------------------------------------------------------------------------- +void library::rebuildEntries(int64_t archive_id, const vector& entries) +{ + auto start_time = app::runTimer(); + + // Delete all existing archive_entry rows for archive_id + deleteArchiveEntryRowsByArchiveId(archive_id); + + // Build list of entry rows to add + vector rows; + for (auto i = 0; i < entries.size(); ++i) + { + rows.emplace_back(archive_id, i, *entries[i]); + entries[i]->setLibraryId(i); + } + + // Add rows to database + insertArchiveEntryRows(rows); + + // Write entry properties to database + saveAllEntryProperties(archive_id, entries); + + log::debug("library::rebuildEntries took {}ms", app::runTimer() - start_time); +} + +void library::saveEntryProperties(int64_t archive_id, const ArchiveEntry& entry) +{ + if (entry.libraryId() < 0) + return; + + // Delete existing rows + db::connectionRW()->exec(fmt::format( + "DELETE FROM archive_entry_property WHERE archive_id = {} AND entry_id = {}", archive_id, entry.libraryId())); + + // Insert prop rows + if (auto sql = db::cacheQuery("insert_archive_entry_property", insert_archive_entry_property, true)) + writeEntryProperties(sql, archive_id, &entry); +} + +void library::saveAllEntryProperties(int64_t archive_id, const vector& entries) +{ + // Delete existing rows + db::connectionRW()->exec(fmt::format("DELETE FROM archive_entry_property WHERE archive_id = {}", archive_id)); + + // Insert prop rows for all entries + if (auto sql = db::cacheQuery("insert_archive_entry_property", insert_archive_entry_property, true)) + { + db::Transaction transaction(db::connectionRW(), false); + transaction.beginIfNoActiveTransaction(); + + for (auto entry : entries) + writeEntryProperties(sql, archive_id, entry); + + transaction.commit(); + } +} + +// ----------------------------------------------------------------------------- +// Reads all entry info from the library for [archive_id] into the given +// [entries]. +// +// Will attempt to match any archive_entry rows that don't match exactly to an +// entry but are close enough (eg. if the entry was renamed or moved since the +// archive was last recorded in the library) +// ----------------------------------------------------------------------------- +void library::readEntryInfo(int64_t archive_id, const vector& entries) +{ + // Get existing archive_entry rows for the archive + auto existing_rows = getArchiveEntryRows(archive_id); + if (existing_rows.empty()) + return; // No rows, so no existing info to read + + auto start_time = app::runTimer(); + + // Find all exact matches (path+name+size+hash is close enough to exact) + // This is likely to account a majority of the entries unless the archive + // has changed a *lot* since it was last saved to the library + unsigned n_unmatched_entries = 0; + unsigned row_start_index = 0; + for (auto entry : entries) + { + entry->setLibraryId(-1); + + for (unsigned i = row_start_index; i < existing_rows.size(); ++i) + { + // Ignore rows already matched to an entry (id < 0) + if (existing_rows[i].id < 0) + { + // If this is the first row being checked, don't check it again + if (row_start_index == i) + ++row_start_index; + + continue; + } + + if (entryRowExactMatch(existing_rows[i], entry->path(), entry->name(), entry->size(), entry->hash())) + { + entry->setLibraryId(existing_rows[i].id); + entry->exProp("TypeHint") = existing_rows[i].type_id; + existing_rows[i].id = -1; + break; + } + } + + if (entry->libraryId() < 0) + ++n_unmatched_entries; + } + + log::debug("archive_entry matching (exact) took {}ms", app::runTimer() - start_time); + + + // For any remaining unmatched entries, try to determine what archive_entry + // row most closely matches the entry (and can reasonably be considered the + // 'same' entry - ideally we should have no match over matching to the wrong + // entry) + if (n_unmatched_entries > 0) + { + start_time = app::runTimer(); + + log::debug( + "Found {} entries in archive {} with no exact-matching archive_entry row", n_unmatched_entries, archive_id); + + // Get list of unmatched entries + vector unmatched_entries; + unmatched_entries.reserve(n_unmatched_entries); + for (auto entry : entries) + if (entry->libraryId() < 0) + unmatched_entries.push_back(entry); + + // Get list of unmatched rows + vector unmatched_rows; + for (auto& row : existing_rows) + if (row.id >= 0) + unmatched_rows.push_back(&row); + + string entry_path, entry_type; + ArchiveEntryRow* current_match; + uint8_t match_score; + bool match_path, match_name, match_data, match_type; + for (auto entry : unmatched_entries) + { + entry_path = entry->path(); + entry_type = entry->type()->id(); + current_match = nullptr; + match_score = 0; + + // Find best match in unmatched rows (if any) + for (auto row : unmatched_rows) + { + if (row->id < 0) + continue; + + // Check what matches the entry + // (zero-sized entries can't be matched by data) + match_path = row->path == entry_path; + match_name = row->name == entry->name(); + match_data = entry->size() > 0 && row->size > 0 && row->hash == entry->hash(); + match_type = row->type_id == entry_type; + + // Renamed entry + if (match_path && match_data) + { + current_match = row; + match_score = 10; + } + + // Moved entry + if (match_score < 9 && match_name && match_data) + { + current_match = row; + match_score = 9; + } + + // Modified entry (same type) + if (match_score < 8 && match_path && match_name && match_type) + { + current_match = row; + match_score = 8; + } + + // Moved+Renamed entry + if (match_score < 7 && match_data) + { + current_match = row; + match_score = 7; + } + } + + if (current_match) + { +#ifdef _DEBUG + static string match_desc; + switch (match_score) + { + case 10: match_desc = "entry renamed"; break; + case 9: match_desc = "entry moved"; break; + case 8: match_desc = "entry modified (same type)"; break; + case 7: match_desc = "entry moved & renamed"; break; + default: match_desc = "unknown match"; break; + } + + log::debug( + "Matched entry {} to row {} ({}) - {}", + entry->path(true), + current_match->id, + current_match->name, + match_desc); +#endif + + entry->setLibraryId(current_match->id); + current_match->id = -1; + } +#ifdef _DEBUG + else + log::debug("No matching row found for entry {}", entry->path(true)); +#endif + } + + log::debug("archive_entry matching (remaining unmatched) took {}ms", app::runTimer() - start_time); + } + else + log::debug("All archive_entry rows in archive {} matched", archive_id); + + + // Load entry properties + for (auto entry : entries) + readEntryProperties(archive_id, entry); + + + // Rebuild entry rows if there were any mismatches + if (n_unmatched_entries > 0) + rebuildEntries(archive_id, entries); +} diff --git a/src/Library/ArchiveEntry.h b/src/Library/ArchiveEntry.h new file mode 100644 index 000000000..b232cefac --- /dev/null +++ b/src/Library/ArchiveEntry.h @@ -0,0 +1,60 @@ +#pragma once + +namespace slade +{ +class ArchiveEntry; + +namespace database +{ + class Context; +} + +namespace library +{ + struct ArchiveEntryRow + { + int64_t archive_id = -1; + int64_t id = -1; + string path; + int index = -1; + string name; + unsigned size = 0; + string hash; + string type_id; + + ArchiveEntryRow() = default; + ArchiveEntryRow( + int64_t archive_id, + int64_t id, + string_view path, + int index, + string_view name, + unsigned size, + string_view hash, + string_view type_id) : + archive_id{ archive_id }, + id{ id }, + path{ path }, + index{ index }, + name{ name }, + size{ size }, + hash{ hash }, + type_id{ type_id } + { + } + ArchiveEntryRow(int64_t archive_id, int64_t id, const ArchiveEntry& entry); + ArchiveEntryRow(database::Context& db, int64_t archive_id, int64_t id); + + int64_t insert() const; + bool update() const; + bool remove(); + }; + + int copyArchiveEntries(int64_t from_archive_id, int64_t to_archive_id); + void rebuildEntries(int64_t archive_id, const vector& entries); + void saveEntryProperties(int64_t archive_id, const ArchiveEntry& entry); + void saveAllEntryProperties(int64_t archive_id, const vector& entries); + void readEntryInfo(int64_t archive_id, const vector& entries); + +} // namespace library +} // namespace slade diff --git a/src/Library/ArchiveFile.cpp b/src/Library/ArchiveFile.cpp new file mode 100644 index 000000000..eea1a188a --- /dev/null +++ b/src/Library/ArchiveFile.cpp @@ -0,0 +1,409 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: ArchiveFile.cpp +// Description: ArchiveFileRow struct and related functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "ArchiveFile.h" +#include "ArchiveEntry.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include "Library.h" +#include "Utility/FileUtils.h" +#include "Utility/StringUtils.h" +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +// SQL query strings +string update_archive_file = + "UPDATE archive_file " + "SET path = ?, size = ?, hash = ?, format_id = ?, last_opened = ?, last_modified = ?, parent_id = ? " + "WHERE id = ?"; +string insert_archive_file = + "REPLACE INTO archive_file (path, size, hash, format_id, last_opened, last_modified, parent_id) " + "VALUES (?,?,?,?,?,?,?)"; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// ArchiveFileRow Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// ArchiveFileRow constructor +// Gets info from the file at [file_path] if it exists +// ----------------------------------------------------------------------------- +ArchiveFileRow::ArchiveFileRow(string_view file_path, string_view format_id) : path{ file_path }, format_id{ format_id } +{ + // Sanitize path + strutil::replaceIP(path, "\\", "/"); + + // Get file info + if (fileutil::fileExists(file_path)) + { + SFile file(file_path); + size = file.size(); + hash = file.calculateHash(); + last_modified = fileutil::fileModifiedTime(file_path); + } +} + +// ----------------------------------------------------------------------------- +// ArchiveFileRow constructor +// Reads existing data from the database. If row [id] doesn't exist in the +// database, the row id will be set to -1 +// ----------------------------------------------------------------------------- +ArchiveFileRow::ArchiveFileRow(database::Context& db, int64_t id) : id{ id } +{ + // Load from database + if (auto sql = db.cacheQuery("get_archive_file", "SELECT * FROM archive_file WHERE id = ?")) + { + sql->clearBindings(); + sql->bind(1, id); + + if (sql->executeStep()) + { + path = sql->getColumn(1).getString(); + size = sql->getColumn(2).getUInt(); + hash = sql->getColumn(3).getString(); + format_id = sql->getColumn(4).getString(); + last_opened = sql->getColumn(5).getInt64(); + last_modified = sql->getColumn(6).getInt64(); + parent_id = sql->getColumn(7).getInt64(); + } + else + { + log::warning("archive_file row with id {} does not exist in the database", id); + this->id = -1; + } + + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// ArchiveFileRow constructor +// Reads data from result columns in the given SQLite statement [sql] +// ----------------------------------------------------------------------------- +ArchiveFileRow::ArchiveFileRow(const SQLite::Statement* sql) +{ + if (!sql) + return; + + id = sql->getColumn(0).getInt64(); + path = sql->getColumn(1).getString(); + size = sql->getColumn(2).getUInt(); + hash = sql->getColumn(3).getString(); + format_id = sql->getColumn(4).getString(); + last_opened = sql->getColumn(5).getInt64(); + last_modified = sql->getColumn(6).getInt64(); + parent_id = sql->getColumn(7).getInt64(); +} + +// ----------------------------------------------------------------------------- +// Inserts this row into the database. +// If successful, id will be updated and returned, otherwise returns -1 +// ----------------------------------------------------------------------------- +int64_t ArchiveFileRow::insert() +{ + if (id >= 0) + { + log::warning("Trying to insert archive_file row id {} that already exists", id); + return id; + } + + if (auto sql = db::cacheQuery("insert_archive_file", insert_archive_file, true)) + { + sql->clearBindings(); + + sql->bind(1, path); + sql->bind(2, size); + sql->bind(3, hash); + sql->bind(4, format_id); + sql->bindDateTime(5, last_opened); + sql->bindDateTime(6, last_modified); + sql->bind(7, parent_id); + + if (sql->exec() > 0) + id = db::connectionRW()->getLastInsertRowid(); + + sql->reset(); + } + + if (id >= 0) + library::signals().archive_file_inserted(id); + + return id; +} + +// ----------------------------------------------------------------------------- +// Updates this row in the database +// ----------------------------------------------------------------------------- +bool ArchiveFileRow::update() const +{ + // Ignore invalid id + if (id < 0) + { + log::warning("Trying to update archive_file row with no id"); + return false; + } + + auto rows = 0; + if (auto sql = db::cacheQuery("update_archive_file", update_archive_file, true)) + { + sql->clearBindings(); + + sql->bind(1, path); + sql->bind(2, size); + sql->bind(3, hash); + sql->bind(4, format_id); + sql->bindDateTime(5, last_opened); + sql->bindDateTime(6, last_modified); + sql->bind(7, parent_id); + sql->bind(8, id); + + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + library::signals().archive_file_updated(id); + return true; + } + + return false; +} + +// ----------------------------------------------------------------------------- +// Removes this row from the database. +// If successful, id will be set to -1 +// ----------------------------------------------------------------------------- +bool ArchiveFileRow::remove() +{ + // Ignore invalid id + if (id < 0) + { + log::warning("Trying to remove archive_file row with no id"); + return false; + } + + auto rows = 0; + + if (auto sql = db::cacheQuery("delete_archive_file", "DELETE FROM archive_file WHERE id = ?")) + { + sql->bind(1, id); + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + library::signals().archive_file_deleted(id); + id = -1; + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Returns the archive_file row id for [filename] (in [parent_id] if given), +// or -1 if it does not exist in the database +// ----------------------------------------------------------------------------- +int64_t library::archiveFileId(string_view filename, int64_t parent_id) +{ + int64_t archive_id = -1; + + if (auto sql = db::cacheQuery("lib_get_archive_id", "SELECT id FROM archive_file WHERE path = ? AND parent_id = ?")) + { + sql->bind(1, filename); + sql->bind(2, parent_id); + + if (sql->executeStep()) + archive_id = sql->getColumn(0); + + sql->reset(); + } + + return archive_id; +} + +// ----------------------------------------------------------------------------- +// Returns the first archive_file row id found that has a matching [size] and +// [hash], or -1 if none found +// ----------------------------------------------------------------------------- +int64_t library::findArchiveFileIdFromData(unsigned size, string_view hash, int64_t parent_id) +{ + int64_t archive_id = -1; + + if (auto sql = db::cacheQuery( + "lib_find_archive_id_data", "SELECT id FROM archive_file WHERE size = ? AND hash = ? AND parent_id = ?")) + { + sql->bind(1, size); + sql->bind(2, hash); + sql->bind(3, parent_id); + + if (sql->executeStep()) + archive_id = sql->getColumn(0); + + sql->reset(); + } + + return archive_id; +} + +// ----------------------------------------------------------------------------- +// Saves [row] to the database, either inserts or updates depending on the id +// ----------------------------------------------------------------------------- +bool library::saveArchiveFile(ArchiveFileRow& row) +{ + if (row.id < 0) + return row.insert() >= 0; + else + return row.update(); +} + +// ----------------------------------------------------------------------------- +// Creates a new archive_file row in the database for [file_path], copying data +// from an existing row [copy_from_id] including any related data eg. +// archive_entry rows. +// Returns the id of the created row or -1 if copy_from_id was invalid +// ----------------------------------------------------------------------------- +int64_t library::copyArchiveFile(string_view file_path, int64_t copy_from_id) +{ + // Get row to copy + ArchiveFileRow archive_file{ db::global(), copy_from_id }; + if (archive_file.id < 0) + return -1; + + // Set path + archive_file.id = -1; + archive_file.path = file_path; + + // Reset last opened time + archive_file.last_opened = 0; + + // Add new archive_file row + auto archive_id = archive_file.insert(); + + // Copy entries + copyArchiveEntries(copy_from_id, archive_id); + + return archive_id; +} + +// ----------------------------------------------------------------------------- +// Removes the archive_file row [id] from the database including all related +// data eg. archive_entry etc. +// ----------------------------------------------------------------------------- +void library::removeArchiveFile(int64_t id) +{ + // Delete row from archive_file + // (all related data will also be removed via cascading foreign keys) + if (db::connectionRW()->exec(fmt::format("DELETE FROM archive_file WHERE id = {}", id)) > 0) + library::signals().archive_file_deleted(id); +} + +// ----------------------------------------------------------------------------- +// Returns the time archive [id] in the library was last opened (as time_t) +// ----------------------------------------------------------------------------- +time_t library::archiveFileLastOpened(int64_t id) +{ + time_t last_opened = 0; + + if (auto sql = db::cacheQuery("get_archive_file_last_opened", "SELECT last_opened FROM archive_file WHERE id = ?")) + { + sql->bind(1, id); + + if (sql->executeStep()) + last_opened = sql->getColumn(0).getInt64(); + + sql->reset(); + } + + return last_opened; +} + +// ----------------------------------------------------------------------------- +// Returns the time archive [id] in the library was last modified on disk +// (as time_t) +// ----------------------------------------------------------------------------- +time_t library::archiveFileLastModified(int64_t id) +{ + time_t last_modified = 0; + + if (auto sql = db::cacheQuery( + "get_archive_file_last_modified", "SELECT last_modified FROM archive_file WHERE id = ?")) + { + sql->bind(1, id); + + if (sql->executeStep()) + last_modified = sql->getColumn(0).getInt64(); + + sql->reset(); + } + + return last_modified; +} + +// ----------------------------------------------------------------------------- +// Returns models for all rows in the archive_file table +// ----------------------------------------------------------------------------- +vector library::allArchiveFileRows() +{ + vector rows; + + if (auto sql = db::cacheQuery("all_archive_file_rows", "SELECT * FROM archive_file")) + { + while (sql->executeStep()) + rows.emplace_back(sql); + } + + return rows; +} diff --git a/src/Library/ArchiveFile.h b/src/Library/ArchiveFile.h new file mode 100644 index 000000000..a6822d70d --- /dev/null +++ b/src/Library/ArchiveFile.h @@ -0,0 +1,66 @@ +#pragma once + +namespace SQLite +{ +class Statement; +} + +namespace slade +{ +namespace database +{ + class Context; +} + +namespace library +{ + // Database model for rows in the archive_file table in the database + struct ArchiveFileRow + { + int64_t id = -1; + string path; + unsigned size = 0; + string hash; + string format_id; + time_t last_opened = 0; + time_t last_modified = 0; + int64_t parent_id = -1; + + ArchiveFileRow() = default; + ArchiveFileRow( + string_view path, + unsigned size, + string_view hash, + string_view format_id, + time_t last_opened, + time_t last_modified, + int64_t parent_id) : + path{ path }, + size{ size }, + hash{ hash }, + format_id{ format_id }, + last_opened{ last_opened }, + last_modified{ last_modified }, + parent_id{ parent_id } + { + } + ArchiveFileRow(string_view file_path, string_view format_id); + ArchiveFileRow(database::Context& db, int64_t id); + ArchiveFileRow(const SQLite::Statement* sql); + + int64_t insert(); + bool update() const; + bool remove(); + }; + + int64_t archiveFileId(string_view filename, int64_t parent_id = -1); + int64_t findArchiveFileIdFromData(unsigned size, string_view hash, int64_t parent_id = -1); + bool saveArchiveFile(ArchiveFileRow& row); + int64_t copyArchiveFile(string_view file_path, int64_t copy_from_id); + void removeArchiveFile(int64_t id); + time_t archiveFileLastOpened(int64_t id); + time_t archiveFileLastModified(int64_t id); + + vector allArchiveFileRows(); +} // namespace library +} // namespace slade diff --git a/src/Library/ArchiveMap.cpp b/src/Library/ArchiveMap.cpp new file mode 100644 index 000000000..87e7027bf --- /dev/null +++ b/src/Library/ArchiveMap.cpp @@ -0,0 +1,220 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: ArchiveMap.cpp +// Description: ArchiveMapRow struct and related functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "ArchiveMap.h" +#include "Archive/ArchiveEntry.h" +#include "Archive/MapDesc.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +string update_archive_map = "UPDATE archive_map SET name = ?, format = ? WHERE archive_id = ? AND header_entry_id = ?"; +string insert_archive_map = "INSERT INTO archive_map (archive_id, header_entry_id, name, format) VALUES (?,?,?,?)"; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// ArchiveMapRow Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// ArchiveMapRow constructor +// Reads existing data from the database. If row [archive_id]+[header_entry_id] +// doesn't exist in the database, the row id will be set to -1 +// ----------------------------------------------------------------------------- +ArchiveMapRow::ArchiveMapRow(database::Context& db, int64_t archive_id, int64_t header_entry_id) +{ + if (auto sql = db.cacheQuery( + "get_archive_map", "SELECT * FROM archive_map WHERE archive_id = ? AND header_entry_id = ?")) + { + sql->bind(1, archive_id); + sql->bind(2, header_entry_id); + + if (sql->executeStep()) + { + this->archive_id = archive_id; + this->header_entry_id = header_entry_id; + name = sql->getColumn(2).getString(); + auto format_int = sql->getColumn(3).getInt(); + + if (format_int <= static_cast(MapFormat::Unknown)) + format = static_cast(format_int); + } + + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// ArchiveMapRow constructor +// Initializes the row with info from [map_desc] +// ----------------------------------------------------------------------------- +ArchiveMapRow::ArchiveMapRow(int64_t archive_id, const MapDesc& map_desc) : archive_id{ archive_id } +{ + auto* head_entry = map_desc.head.lock().get(); + if (!head_entry) + { + this->archive_id = -1; + return; + } + + header_entry_id = head_entry->libraryId(); + name = map_desc.name; + format = map_desc.format; +} + +// ----------------------------------------------------------------------------- +// Inserts this row into the database. +// If successful, the inserted row id is returned, otherwise returns -1. +// (note the returned row id isn't the 'id' column since that is not a primary +// key, it returns the sqlite rowid of the row) +// ----------------------------------------------------------------------------- +int64_t ArchiveMapRow::insert() +{ + int64_t row_id = -1; + + if (auto sql = db::cacheQuery("insert_archive_map", insert_archive_map, true)) + { + sql->bind(1, archive_id); + sql->bind(2, header_entry_id); + sql->bind(3, name); + sql->bind(4, static_cast(format)); + + if (sql->exec() > 0) + row_id = db::connectionRW()->getLastInsertRowid(); + + sql->reset(); + } + + return row_id; +} + +// ----------------------------------------------------------------------------- +// Updates this row in the database +// ----------------------------------------------------------------------------- +bool ArchiveMapRow::update() const +{ + // Ignore invalid id + if (archive_id < 0 || header_entry_id < 0) + { + log::warning("Trying to update archive_map row with no archive+entry id"); + return false; + } + + auto rows = 0; + + if (auto sql = db::cacheQuery("update_archive_map", update_archive_map, true)) + { + sql->bind(1, name); + sql->bind(2, static_cast(format)); + sql->bind(3, archive_id); + sql->bind(4, header_entry_id); + + rows = sql->exec(); + + sql->reset(); + } + + return rows > 0; +} + +// ----------------------------------------------------------------------------- +// Removes this row from the database. +// If successful, archive_id and header_entry_id will be set to -1 +// ----------------------------------------------------------------------------- +bool ArchiveMapRow::remove() +{ + // Ignore invalid id + if (archive_id < 0 || header_entry_id < 0) + { + log::warning("Trying to remove archive_map row with no archive+entry id"); + return false; + } + + auto rows = 0; + + if (auto sql = db::cacheQuery( + "delete_archive_map", "DELETE FROM archive_map WHERE archive_id = ? AND header_entry_id = ?")) + { + sql->bind(1, archive_id); + sql->bind(2, header_entry_id); + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + archive_id = -1; + header_entry_id = -1; + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Rebuilds all archive_map rows for [archive_id] from [archive] +// ----------------------------------------------------------------------------- +void library::updateArchiveMaps(int64_t archive_id, const Archive& archive) +{ + auto maps = archive.detectMaps(); + + // Delete existing map rows + db::exec(fmt::format("DELETE FROM archive_map WHERE archive_id = {}", archive_id)); + + // Add detected maps to database + for (const auto& map : maps) + { + ArchiveMapRow row{ archive_id, map }; + row.insert(); + } +} diff --git a/src/Library/ArchiveMap.h b/src/Library/ArchiveMap.h new file mode 100644 index 000000000..388df4ab2 --- /dev/null +++ b/src/Library/ArchiveMap.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Archive/Archive.h" +#include "General/Defs.h" + +namespace slade +{ +namespace database +{ + class Context; +} + +namespace library +{ + struct ArchiveMapRow + { + int64_t archive_id = -1; + int64_t header_entry_id = -1; + string name; + MapFormat format = MapFormat::Unknown; + + ArchiveMapRow(database::Context& db, int64_t archive_id, int64_t header_entry_id); + ArchiveMapRow(int64_t archive_id, const MapDesc& map_desc); + + int64_t insert(); + bool update() const; + bool remove(); + }; + + void updateArchiveMaps(int64_t archive_id, const Archive& archive); +} // namespace library +} // namespace slade diff --git a/src/Library/ArchiveMapConfig.cpp b/src/Library/ArchiveMapConfig.cpp new file mode 100644 index 000000000..6bf8ae963 --- /dev/null +++ b/src/Library/ArchiveMapConfig.cpp @@ -0,0 +1,200 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: ArchiveMapConfig.cpp +// Description: ArchiveMapConfigRow struct and related functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "ArchiveMapConfig.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +string insert_archive_map_config = "INSERT INTO archive_map_config VALUES (?,?,?)"; +string update_archive_map_config = + "UPDATE archive_map_config " + "SET game = ?, port = ? " + "WHERE archive_id = ?"; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// ArchiveMapConfigRow Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// ArchiveMapConfigRow constructor +// Reads existing data from the database. If a row with [archive_id] doesn't +// exist in the database, the row archive_id will be set to -1 +// ----------------------------------------------------------------------------- +ArchiveMapConfigRow::ArchiveMapConfigRow(database::Context& db, int64_t archive_id) +{ + if (auto sql = db.cacheQuery("get_archive_map_config", "SELECT * FROM archive_map_config WHERE archive_id = ?")) + { + sql->bind(1, archive_id); + + if (sql->executeStep()) + { + this->archive_id = archive_id; + game = sql->getColumn(1).getString(); + port = sql->getColumn(2).getString(); + } + + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Inserts this row into the database. +// If successful, the inserted row id is returned, otherwise returns -1 +// ----------------------------------------------------------------------------- +int64_t ArchiveMapConfigRow::insert() const +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to insert archive_map_config row with no archive_id"); + return false; + } + + int64_t row_id = -1; + + if (auto sql = db::cacheQuery("insert_archive_map_config", insert_archive_map_config, true)) + { + sql->bind(1, archive_id); + sql->bind(2, game); + sql->bind(3, port); + + if (sql->exec() > 0) + row_id = db::connectionRW()->getLastInsertRowid(); + + sql->reset(); + } + + return row_id; +} + +// ----------------------------------------------------------------------------- +// Updates this row in the database +// ----------------------------------------------------------------------------- +bool ArchiveMapConfigRow::update() const +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to update archive_map_config row with no archive_id"); + return false; + } + + int rows = 0; + + if (auto sql = db::cacheQuery("update_archive_map_config", update_archive_map_config, true)) + { + sql->bind(1, game); + sql->bind(2, port); + sql->bind(3, archive_id); + + rows = sql->exec(); + + sql->reset(); + } + + return rows > 0; +} + +// ----------------------------------------------------------------------------- +// Removes this row from the database. +// If successful, archive_id will be set to -1 +// ----------------------------------------------------------------------------- +bool ArchiveMapConfigRow::remove() +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to delete archive_map_config row with no archive_id"); + return false; + } + + auto rows = 0; + if (auto sql = db::cacheQuery("delete_archive_map_config", "DELETE FROM archive_map_config WHERE archive_id = ?")) + { + sql->bind(1, archive_id); + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + archive_id = -1; + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Returns the archive_map_config row for [archive_id]. +// If it doesn't exist in the database, the row's archive_id will be -1 +// ----------------------------------------------------------------------------- +ArchiveMapConfigRow library::getArchiveMapConfig(int64_t archive_id) +{ + return { db::global(), archive_id }; +} + +// ----------------------------------------------------------------------------- +// Saves [row] to the database, either inserts or updates if the row for +// archive_id already exists +// ----------------------------------------------------------------------------- +bool library::saveArchiveMapConfig(const ArchiveMapConfigRow& row) +{ + if (row.archive_id < 0) + return false; + + // Update/Insert + return db::rowIdExists("archive_map_config", row.archive_id, "archive_id") ? row.update() : row.insert() >= 0; +} diff --git a/src/Library/ArchiveMapConfig.h b/src/Library/ArchiveMapConfig.h new file mode 100644 index 000000000..c29defaeb --- /dev/null +++ b/src/Library/ArchiveMapConfig.h @@ -0,0 +1,30 @@ +#pragma once + +namespace slade +{ +namespace database +{ + class Context; +} + +namespace library +{ + struct ArchiveMapConfigRow + { + int64_t archive_id = -1; + string game; + string port; + + ArchiveMapConfigRow() = default; + ArchiveMapConfigRow(database::Context& db, int64_t archive_id); + ArchiveMapConfigRow(int64_t archive_id) : archive_id{ archive_id } {} + + int64_t insert() const; + bool update() const; + bool remove(); + }; + + ArchiveMapConfigRow getArchiveMapConfig(int64_t archive_id); + bool saveArchiveMapConfig(const ArchiveMapConfigRow& row); +} // namespace library +} // namespace slade diff --git a/src/Library/ArchiveRunConfig.cpp b/src/Library/ArchiveRunConfig.cpp new file mode 100644 index 000000000..30e8cf7a3 --- /dev/null +++ b/src/Library/ArchiveRunConfig.cpp @@ -0,0 +1,206 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: ArchiveRunConfig.cpp +// Description: ArchiveRunConfigRow struct and related functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "ArchiveRunConfig.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +string insert_archive_run_config = "INSERT INTO archive_run_config VALUES (?,?,?,?,?)"; +string update_archive_run_config = + "UPDATE archive_run_config " + "SET executable_id = ?, run_config = ?, run_extra = ?, iwad_path = ? " + "WHERE archive_id = ?"; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// ArchiveRunConfigRow Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// ArchiveRunConfigRow constructor +// Reads existing data from the database. If a row with [archive_id] doesn't +// exist in the database, the row archive_id will be set to -1 +// ----------------------------------------------------------------------------- +ArchiveRunConfigRow::ArchiveRunConfigRow(database::Context& db, int64_t archive_id) +{ + if (auto sql = db.cacheQuery("get_archive_run_config", "SELECT * FROM archive_run_config WHERE archive_id = ?")) + { + sql->bind(1, archive_id); + + if (sql->executeStep()) + { + this->archive_id = archive_id; + executable_id = sql->getColumn(1).getString(); + run_config = sql->getColumn(2).getInt(); + run_extra = sql->getColumn(3).getString(); + iwad_path = sql->getColumn(4).getString(); + } + + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Inserts this row into the database. +// If successful, the inserted row id is returned, otherwise returns -1 +// ----------------------------------------------------------------------------- +int64_t ArchiveRunConfigRow::insert() const +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to insert archive_run_config row with no archive_id"); + return false; + } + + int64_t row_id = -1; + + if (auto sql = db::cacheQuery("insert_archive_run_config", insert_archive_run_config, true)) + { + sql->bind(1, archive_id); + sql->bind(2, executable_id); + sql->bind(3, run_config); + sql->bind(4, run_extra); + sql->bind(5, iwad_path); + + if (sql->exec() > 0) + row_id = db::connectionRW()->getLastInsertRowid(); + + sql->reset(); + } + + return row_id; +} + +// ----------------------------------------------------------------------------- +// Updates this row in the database +// ----------------------------------------------------------------------------- +bool ArchiveRunConfigRow::update() const +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to update archive_run_config row with no archive_id"); + return false; + } + + int rows = 0; + + if (auto sql = db::cacheQuery("update_archive_run_config", update_archive_run_config, true)) + { + sql->bind(1, executable_id); + sql->bind(2, run_config); + sql->bind(3, run_extra); + sql->bind(4, iwad_path); + sql->bind(5, archive_id); + + rows = sql->exec(); + + sql->reset(); + } + + return rows > 0; +} + +// ----------------------------------------------------------------------------- +// Removes this row from the database. +// If successful, archive_id will be set to -1 +// ----------------------------------------------------------------------------- +bool ArchiveRunConfigRow::remove() +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to delete archive_run_config row with no archive_id"); + return false; + } + + auto rows = 0; + if (auto sql = db::cacheQuery("delete_archive_run_config", "DELETE FROM archive_run_config WHERE archive_id = ?")) + { + sql->bind(1, archive_id); + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + archive_id = -1; + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Returns the archive_run_config row for [archive_id]. +// If it doesn't exist in the database, the row's archive_id will be -1 +// ----------------------------------------------------------------------------- +ArchiveRunConfigRow library::getArchiveRunConfig(int64_t archive_id) +{ + return { db::global(), archive_id }; +} + +// ----------------------------------------------------------------------------- +// Saves [row] to the database, either inserts or updates if the row for +// archive_id already exists +// ----------------------------------------------------------------------------- +bool library::saveArchiveRunConfig(const ArchiveRunConfigRow& row) +{ + if (row.archive_id < 0) + return false; + + // Update/Insert + return db::rowIdExists("archive_run_config", row.archive_id, "archive_id") ? row.update() : row.insert() >= 0; +} diff --git a/src/Library/ArchiveRunConfig.h b/src/Library/ArchiveRunConfig.h new file mode 100644 index 000000000..b8a4e7ba2 --- /dev/null +++ b/src/Library/ArchiveRunConfig.h @@ -0,0 +1,32 @@ +#pragma once + +namespace slade +{ +namespace database +{ + class Context; +} + +namespace library +{ + struct ArchiveRunConfigRow + { + int64_t archive_id = -1; + string executable_id; + int run_config = 0; + string run_extra; + string iwad_path; + + ArchiveRunConfigRow() = default; + ArchiveRunConfigRow(database::Context& db, int64_t archive_id); + ArchiveRunConfigRow(int64_t archive_id) : archive_id{ archive_id } {} + + int64_t insert() const; + bool update() const; + bool remove(); + }; + + ArchiveRunConfigRow getArchiveRunConfig(int64_t archive_id); + bool saveArchiveRunConfig(const ArchiveRunConfigRow& row); +} // namespace library +} // namespace slade diff --git a/src/Library/ArchiveUIConfig.cpp b/src/Library/ArchiveUIConfig.cpp new file mode 100644 index 000000000..7edfc7297 --- /dev/null +++ b/src/Library/ArchiveUIConfig.cpp @@ -0,0 +1,303 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: ArchiveUIConfig.cpp +// Description: ArchiveUIConfigRow struct and related functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "ArchiveUIConfig.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include "UI/State.h" +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +string update_archive_ui_config = + "UPDATE archive_ui_config " + "SET elist_index_visible = ?, elist_index_width = ?, elist_name_width = ?, elist_size_visible = ?, " + " elist_size_width = ?, elist_type_visible = ?, elist_type_width = ?, elist_sort_column = ?, " + " elist_sort_descending = ?, splitter_position = ? " + "WHERE archive_id = ?"; +string insert_archive_ui_config = + "INSERT INTO archive_ui_config (archive_id, elist_index_visible, elist_index_width, elist_name_width, " + " elist_size_visible, elist_size_width, elist_type_visible, elist_type_width, " + " elist_sort_column, elist_sort_descending, splitter_position) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?)"; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// ArchiveUIConfigRow Struct Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// ArchiveUIConfigRow constructor +// Reads existing data from the database. If a row with [archive_id] doesn't +// exist in the database, the row archive_id will be set to -1 +// ----------------------------------------------------------------------------- +ArchiveUIConfigRow::ArchiveUIConfigRow(db::Context& db, int64_t archive_id) : archive_id{ archive_id } +{ + if (auto sql = db.cacheQuery("get_archive_ui_config", "SELECT * FROM archive_ui_config WHERE archive_id = ?")) + { + sql->clearBindings(); + sql->bind(1, archive_id); + + if (sql->executeStep()) + { + elist_index_visible = sql->getColumn(1).getInt() > 0; + elist_index_width = sql->getColumn(2).getInt(); + elist_name_width = sql->getColumn(3).getInt(); + elist_size_visible = sql->getColumn(4).getInt() > 0; + elist_size_width = sql->getColumn(5).getInt(); + elist_type_visible = sql->getColumn(6).getInt() > 0; + elist_type_width = sql->getColumn(7).getInt(); + elist_sort_column = sql->getColumn(8).getString(); + elist_sort_descending = sql->getColumn(9).getInt() > 0; + splitter_position = sql->getColumn(10).getInt(); + } + else + { + log::warning("archive_ui_config row with archive_id {} does not exist in the database", archive_id); + this->archive_id = -1; + } + + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// ArchiveUIConfigRow constructor +// Initializes the row for [archive_id] with 'default' values taken from cvars +// depending on [tree_view] +// ----------------------------------------------------------------------------- +ArchiveUIConfigRow::ArchiveUIConfigRow(int64_t archive_id, bool tree_view) : archive_id{ archive_id } +{ + elist_index_visible = ui::getStateBool("EntryListIndexVisible"); + elist_index_width = ui::getStateInt("EntryListIndexWidth"); + elist_name_width = ui::getStateInt(tree_view ? "EntryListNameWidthTree" : "EntryListNameWidthList"); + elist_size_visible = ui::getStateBool("EntryListSizeVisible"); + elist_size_width = ui::getStateInt("EntryListSizeWidth"); + elist_type_visible = ui::getStateBool("EntryListTypeVisible"); + elist_type_width = ui::getStateInt("EntryListTypeWidth"); + splitter_position = ui::getStateInt(tree_view ? "ArchivePanelSplitPosTree" : "ArchivePanelSplitPosList"); + + log::debug("Created default entry list config for archive {}", archive_id); +} + +// ----------------------------------------------------------------------------- +// Inserts this row into the database. +// If successful, the inserted row id is returned, otherwise returns -1 +// ----------------------------------------------------------------------------- +int64_t ArchiveUIConfigRow::insert() const +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to insert archive_ui_config row with no archive_id"); + return false; + } + + int64_t row_id = -1; + + if (auto sql = db::cacheQuery("insert_archive_ui_config", insert_archive_ui_config, true)) + { + sql->clearBindings(); + + sql->bind(1, archive_id); + sql->bind(2, elist_index_visible); + sql->bind(3, elist_index_width); + sql->bind(4, elist_name_width); + sql->bind(5, elist_size_visible); + sql->bind(6, elist_size_width); + sql->bind(7, elist_type_visible); + sql->bind(8, elist_type_width); + sql->bind(9, elist_sort_column); + sql->bind(10, elist_sort_descending); + sql->bind(11, splitter_position); + + if (sql->exec() > 0) + row_id = db::connectionRW()->getLastInsertRowid(); + + sql->reset(); + } + + return row_id; +} + +// ----------------------------------------------------------------------------- +// Updates this row in the database +// ----------------------------------------------------------------------------- +bool ArchiveUIConfigRow::update() const +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to update archive_ui_config row with no archive_id"); + return false; + } + + auto rows = 0; + if (auto sql = db::cacheQuery("update_archive_ui_config", update_archive_ui_config, true)) + { + sql->clearBindings(); + + sql->bind(1, elist_index_visible); + sql->bind(2, elist_index_width); + sql->bind(3, elist_name_width); + sql->bind(4, elist_size_visible); + sql->bind(5, elist_size_width); + sql->bind(6, elist_type_visible); + sql->bind(7, elist_type_width); + sql->bind(8, elist_sort_column); + sql->bind(9, elist_sort_descending); + sql->bind(10, splitter_position); + sql->bind(11, archive_id); + + rows = sql->exec(); + sql->reset(); + } + + return rows > 0; +} + +// ----------------------------------------------------------------------------- +// Removes this row from the database. +// If successful, archive_id will be set to -1 +// ----------------------------------------------------------------------------- +bool ArchiveUIConfigRow::remove() +{ + // Ignore invalid id + if (archive_id < 0) + { + log::warning("Trying to delete archive_ui_config row with no archive_id"); + return false; + } + + auto rows = 0; + if (auto sql = db::cacheQuery("delete_archive_ui_config", "DELETE FROM archive_ui_config WHERE archive_id = ?")) + { + sql->bind(1, archive_id); + rows = sql->exec(); + sql->reset(); + } + + if (rows > 0) + { + archive_id = -1; + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Returns the archive_ui_config row for [archive_id]. +// If it doesn't exist in the database, the row's archive_id will be -1 +// ----------------------------------------------------------------------------- +ArchiveUIConfigRow library::getArchiveUIConfig(int64_t archive_id) +{ + return { db::global(), archive_id }; +} + +// ----------------------------------------------------------------------------- +// Saves [row] to the database, either inserts or updates if the row for +// archive_id already exists +// ----------------------------------------------------------------------------- +bool library::saveArchiveUIConfig(const ArchiveUIConfigRow& row) +{ + if (row.archive_id < 0) + return false; + + // Update/Insert + return db::rowIdExists("archive_ui_config", row.archive_id, "archive_id") ? row.update() : row.insert() >= 0; +} + +// ----------------------------------------------------------------------------- +// Returns the splitter position for [archive_id], or -1 if no config exists +// ----------------------------------------------------------------------------- +int library::archiveUIConfigSplitterPos(int64_t archive_id) +{ + int splitter_pos = -1; + + if (auto sql = db::cacheQuery( + "archive_ui_config_splitter_pos", "SELECT splitter_position FROM archive_ui_config WHERE archive_id = ?")) + { + sql->clearBindings(); + sql->bind(1, archive_id); + + if (sql->executeStep()) + splitter_pos = sql->getColumn(0).getInt(); + + sql->reset(); + } + + return splitter_pos; +} + +// ----------------------------------------------------------------------------- +// Saves the splitter position for [archive_id] +// ----------------------------------------------------------------------------- +bool library::saveArchiveUIConfigSplitterPos(int64_t archive_id, int splitter_pos) +{ + bool updated = false; + + if (auto sql = db::cacheQuery( + "update_archive_ui_config_splitter_position", + "UPDATE archive_ui_config SET splitter_position = ? WHERE archive_id = ?", + true)) + { + sql->clearBindings(); + sql->bind(1, splitter_pos); + sql->bind(2, archive_id); + + updated = sql->exec() > 0; + + sql->reset(); + } + + return updated; +} diff --git a/src/Library/ArchiveUIConfig.h b/src/Library/ArchiveUIConfig.h new file mode 100644 index 000000000..fa4f97ebe --- /dev/null +++ b/src/Library/ArchiveUIConfig.h @@ -0,0 +1,68 @@ +#pragma once + +namespace slade +{ +namespace database +{ + class Context; +} + +namespace library +{ + // Database model for rows in the archive_ui_config table in the database + struct ArchiveUIConfigRow + { + int64_t archive_id = -1; + bool elist_index_visible = false; + int elist_index_width = -1; + int elist_name_width = -1; + bool elist_size_visible = true; + int elist_size_width = -1; + bool elist_type_visible = true; + int elist_type_width = -1; + string elist_sort_column; + bool elist_sort_descending = false; + int splitter_position = -1; + + ArchiveUIConfigRow() = default; + ArchiveUIConfigRow(int64_t archive_id) : archive_id{ archive_id } {} + ArchiveUIConfigRow( + int64_t archive_id, + bool elist_index_visible, + int elist_index_width, + int elist_name_width, + bool elist_size_visible, + int elist_size_width, + bool elist_type_visible, + int elist_type_width, + string_view elist_sort_column, + bool elist_sort_descending, + int splitter_position) : + archive_id{ archive_id }, + elist_index_visible{ elist_index_visible }, + elist_index_width{ elist_index_width }, + elist_name_width{ elist_name_width }, + elist_size_visible{ elist_size_visible }, + elist_size_width{ elist_size_width }, + elist_type_visible{ elist_type_visible }, + elist_type_width{ elist_type_width }, + elist_sort_column{ elist_sort_column }, + elist_sort_descending{ elist_sort_descending }, + splitter_position{ splitter_position } + { + } + ArchiveUIConfigRow(database::Context& db, int64_t archive_id); + ArchiveUIConfigRow(int64_t archive_id, bool tree_view); + + int64_t insert() const; + bool update() const; + bool remove(); + }; + + ArchiveUIConfigRow getArchiveUIConfig(int64_t archive_id); + bool saveArchiveUIConfig(const ArchiveUIConfigRow& row); + int archiveUIConfigSplitterPos(int64_t archive_id); + bool saveArchiveUIConfigSplitterPos(int64_t archive_id, int splitter_pos); + +} // namespace library +} // namespace slade diff --git a/src/Library/Library.cpp b/src/Library/Library.cpp new file mode 100644 index 000000000..ff4078679 --- /dev/null +++ b/src/Library/Library.cpp @@ -0,0 +1,691 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: Library.cpp +// Description: Functions dealing with the program's archive 'library', which is +// essentially a bunch of info about archives that have been opened +// in SLADE. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "Library.h" +#include "App.h" +#include "Archive/Archive.h" +#include "Archive/ArchiveEntry.h" +#include "Archive/ArchiveFormat.h" +#include "Archive/ArchiveFormatHandler.h" +#include "ArchiveEntry.h" +#include "ArchiveFile.h" +#include "ArchiveMap.h" +#include "ArchiveUIConfig.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include "Database/Transaction.h" +#include "Utility/FileUtils.h" +#include "Utility/StringUtils.h" +#include "Utility/Tokenizer.h" +#include + +using namespace slade; +using namespace library; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace slade::library +{ +Signals lib_signals; +std::atomic lib_scan_running{ false }; // Whether a library scan is currently running, set to false to request stop + +string insert_archive_bookmark = "INSERT OR REPLACE INTO archive_bookmark VALUES (?,?)"; + +vector recent_files; +} // namespace slade::library + + +// ----------------------------------------------------------------------------- +// +// library Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Initializes the library +// ----------------------------------------------------------------------------- +void library::init() +{ + // Remove pre-3.3.0 recent files that exist in the library + // (and have been opened) + vector rf_to_remove; + for (const auto& recent_file : recent_files) + { + // Find archive_file row id from path + auto archive_id = archiveFileId(recent_file); + if (archive_id < 0) + continue; + + // Load row details + ArchiveFileRow archive_file{ db::global(), archive_id }; + + // If the last_opened time exists, we can remove it from the pre-3.3.0 + // recent files list + if (archive_file.last_opened > 0) + rf_to_remove.push_back(recent_file); + } + for (const auto& path : rf_to_remove) + VECTOR_REMOVE(recent_files, path); +} + +// ----------------------------------------------------------------------------- +// Reads all info from the library about the given [archive]. +// Returns the library id of the archive +// ----------------------------------------------------------------------------- +int64_t library::readArchiveInfo(const Archive& archive) +{ + try + { + // Find archive_file row for [archive] --------------------------------- + + // Create row from file path to use for comparison + auto archive_file = ArchiveFileRow{ archive.filename(), archive.formatId() }; + + // Check for parent archive + if (auto* parent = archive.parentArchive()) + { + auto* entry = archive.parentEntry(); + archive_file.parent_id = parent->libraryId(); + archive_file.path = parent->filename() + "/" + entry->name(); + } + + // Find existing archive_file row id for the archive's filename + auto archive_id = archiveFileId(archive_file.path, archive_file.parent_id); + if (archive_id < 0) + { + // Not found - look for match + + // Can't match folder archives by data (yet) + // TODO: Figure out a good way to store size/hash for folder archive + if (archive_file.format_id == "folder") + return -1; + + // Find archive_file row with matching data + // If no data match found this archive doesn't exist in the library + auto match_id = findArchiveFileIdFromData(archive_file.size, archive_file.hash, archive_file.parent_id); + if (match_id < 0) + return -1; + + // Check if the matched file exists on disk (or in the parent archive) + // - If it exists the archive has likely been copied so copy its + // data in the library + // - If it doesn't exist the archive has likely been moved so just + // use and update the existing (matched) row in the library + auto match_row = ArchiveFileRow{ db::global(), match_id }; + if (archive_file.parent_id < 0 && fileutil::fileExists(match_row.path)) + archive_id = copyArchiveFile(archive_file.path, match_id); + else if (archive_file.parent_id >= 0 && archive.parentArchive()->entryAtPath(archive_file.path)) + archive_id = copyArchiveFile(archive_file.path, match_id); + else + { + archive_id = match_id; + + // Update existing row with new file details + auto existing_row = ArchiveFileRow{ db::global(), archive_id }; + existing_row.path = archive_file.path; + existing_row.last_modified = archive_file.last_modified; + existing_row.format_id = archive_file.format_id; + existing_row.update(); + } + } + + // Read archive_entry rows for archive --------------------------------- + + vector all_entries; + archive.putEntryTreeAsList(all_entries); + readEntryInfo(archive_id, all_entries); + + + // Finish up + archive.setLibraryId(archive_id); + return archive_id; + } + catch (SQLite::Exception& ex) + { + log::error("Error reading archive info from the library: {}", ex.what()); + return -1; + } +} + +// ----------------------------------------------------------------------------- +// Sets the [last_opened] time for [archive_id] in the library +// ----------------------------------------------------------------------------- +void library::setArchiveLastOpenedTime(int64_t archive_id, time_t last_opened) +{ + try + { + if (auto sql = db::cacheQuery( + "lib_set_archive_last_opened", "UPDATE archive_file SET last_opened = ? WHERE id = ?", true)) + { + sql->bindDateTime(1, last_opened); + sql->bind(2, archive_id); + sql->exec(); + sql->reset(); + } + + lib_signals.archive_file_updated(archive_id); + } + catch (SQLite::Exception& ex) + { + log::error("Error setting archive last opened time in library: {}", ex.what()); + } +} + +// ----------------------------------------------------------------------------- +// Writes all info for [archive] into the library. +// Returns the library id of the archive +// ----------------------------------------------------------------------------- +int64_t library::writeArchiveInfo(const Archive& archive) +{ + try + { + // Create row from archive path + format + auto archive_file = ArchiveFileRow{ archive.filename(), archive.formatId() }; + + // Check for parent archive + if (auto* parent = archive.parentArchive()) + { + auto* entry = archive.parentEntry(); + archive_file.parent_id = parent->libraryId(); + archive_file.path = parent->filename() + "/" + entry->name(); + archive_file.size = entry->size(); + archive_file.hash = entry->hash(); + } + + // Get id of row in database if it exists + archive_file.id = archiveFileId(archive_file.path, archive_file.parent_id); + auto new_archive_row = archive_file.id < 0; + + // Keep last opened time if the row exists + if (!new_archive_row) + archive_file.last_opened = archiveFileLastOpened(archive_file.id); + + // Write row to database + saveArchiveFile(archive_file); + + // Create archive_ui_config row if needed + if (new_archive_row) + { + ArchiveUIConfigRow ui_config{ archive_file.id, archive.formatInfo().supports_dirs }; + ui_config.insert(); + } + + // Write entries to database + vector all_entries; + archive.putEntryTreeAsList(all_entries); + rebuildEntries(archive_file.id, all_entries); + + // Write maps to database + updateArchiveMaps(archive_file.id, archive); + + // Remove from pre-3.3.0 recent files list if it's there + if (auto rf = find(recent_files.begin(), recent_files.end(), archive_file.path); rf != recent_files.end()) + recent_files.erase(rf); + + // Finish up + archive.setLibraryId(archive_file.id); + return archive_file.id; + } + catch (SQLite::Exception& ex) + { + log::error("Error writing archive info to the library: {}", ex.what()); + return -1; + } +} + +// ----------------------------------------------------------------------------- +// (Re)Writes all info for [archive]'s entries into the library +// ----------------------------------------------------------------------------- +void library::writeArchiveEntryInfo(const Archive& archive) +{ + // If it doesn't exist in the library need to add it + if (archive.libraryId() < 0) + { + writeArchiveInfo(archive); + return; + } + + try + { + // Write entries to database + vector all_entries; + archive.putEntryTreeAsList(all_entries); + rebuildEntries(archive.libraryId(), all_entries); + } + catch (SQLite::Exception& ex) + { + log::error("Error writing archive entry info to the library: {}", ex.what()); + } +} + +// ----------------------------------------------------------------------------- +// (Re)Writes all info for [archive]'s maps into the library +// ----------------------------------------------------------------------------- +void library::writeArchiveMapInfo(const Archive& archive) +{ + // If it doesn't exist in the library need to add it + if (archive.libraryId() < 0) + { + writeArchiveInfo(archive); + return; + } + + try + { + // Write maps to database + updateArchiveMaps(archive.libraryId(), archive); + } + catch (SQLite::Exception& ex) + { + log::error("Error writing archive map info to the library: {}", ex.what()); + } +} + +// ----------------------------------------------------------------------------- +// Removes all archives in the library that no longer exist on disk +// ----------------------------------------------------------------------------- +void library::removeMissingArchives() +{ + try + { + vector to_remove; + + if (auto sql = db::cacheQuery("lib_all_archive_paths", "SELECT id, path FROM archive_file WHERE parent_id < 0")) + { + while (sql->executeStep()) + { + if (!fileutil::fileExists(sql->getColumn(1).getString())) + to_remove.push_back(sql->getColumn(0).getInt64()); + } + + sql->reset(); + } + + for (auto id : to_remove) + { + log::info("Removing archive {} from library (no longer exists)", id); + removeArchiveFile(id); + } + } + catch (SQLite::Exception& ex) + { + log::error("Error removing missing archives from the library: {}", ex.what()); + } +} + +// ----------------------------------------------------------------------------- +// Returns the filenames of the [count] most recently opened archives +// ----------------------------------------------------------------------------- +vector library::recentFiles(unsigned count) +{ + vector paths; + + try + { + // Get or create cached query to select base resource paths + if (auto sql = db::cacheQuery( + "lib_recent_files", + "SELECT path FROM archive_file " + "WHERE last_opened > 0 AND parent_id < 0 " + "ORDER BY last_opened DESC LIMIT ?")) + { + sql->bind(1, count); + + // Execute query and add results to list + while (sql->executeStep()) + paths.push_back(sql->getColumn(0).getString()); + + sql->reset(); + } + } + catch (SQLite::Exception& ex) + { + log::error("Error getting recent files from the library: {}", ex.what()); + } + + // Append pre-3.3.0 recent files list to make up count if required + if (!recent_files.empty() && paths.size() < count) + { + for (const auto& recent_file : recent_files) + { + if (paths.size() == count) + break; + + paths.push_back(recent_file); + } + } + + return paths; +} + +// ----------------------------------------------------------------------------- +// Finds and scans all archives in [path] (recursively), adding or updating them +// in the library. Files with extensions in [ignore_ext] will be ignored. +// +// This is safe to run in a background thread, and only one scan can be running +// at any time +// ----------------------------------------------------------------------------- +void library::scanArchivesInDir(string_view path, const vector& ignore_ext, bool rebuild) +{ + if (lib_scan_running) + { + // Abort if a scan is already running (eg. in another thread) + log::warning("Library scan already running, can only have one running at once"); + return; + } + + auto files = fileutil::allFilesInDir(path, true); + + lib_scan_running = true; + + for (auto& filename : files) + { + // Sanitize path + strutil::replaceIP(filename, "\\", "/"); + + // Check extension + auto ext = strutil::Path::extensionOf(filename); + if (VECTOR_EXISTS(ignore_ext, ext)) + { + log::debug("File {} has ignored extension, skipping", filename); + continue; + } + if (!archive::isKnownExtension(ext)) + { + log::debug("File {} has unknown archive extension, skipping", filename); + continue; + } + + // Check if the file exists in the library + auto lib_id = archiveFileId(filename); + if (lib_id >= 0 && !rebuild) + { + // Check if the file on disk hasn't been modified since it was last updated in the library + auto lib_file_modified = archiveFileLastModified(lib_id); + if (lib_file_modified == fileutil::fileModifiedTime(filename)) + { + log::info( + "Library Scan: File {} is already in library and has not been modified since last scanned", + filename); + continue; + } + } + + // Check if file is a known archive format + auto format = archive::detectArchiveFormat(filename); + if (format != ArchiveFormat::Unknown) + { + auto archive = std::make_unique(format); + + log::info("Library Scan: Scanning file \"{}\" (detected as {})", filename, archive->formatInfo().name); + + if (!archive->open(filename, true)) + { + log::info("Library Scan: Failed to open archive file {}: {}", filename, global::error); + continue; + } + + auto id = readArchiveInfo(*archive); + if (id < 0 || rebuild) + { + if (!rebuild) + log::info("Library Scan: Archive file doesn't exist in library, adding"); + + writeArchiveInfo(*archive); + } + else + log::info("Library Scan: Archive already exists in library"); + } + else + log::debug("File {} is not a known/valid archive format, skipping", filename); + + // Check if stop scan was requested + if (!lib_scan_running) + { + log::info("Library Scan: Stop scan requested, ending scan"); + return; + } + } + + lib_scan_running = false; +} + +// ----------------------------------------------------------------------------- +// Stops the currently running library scan (if any) +// ----------------------------------------------------------------------------- +void library::stopArchiveDirScan() +{ + if (lib_scan_running) + lib_scan_running = false; +} + +// ----------------------------------------------------------------------------- +// Returns true if a library scan is currently running +// ----------------------------------------------------------------------------- +bool library::archiveDirScanRunning() +{ + return lib_scan_running; +} + +// ----------------------------------------------------------------------------- +// Returns all bookmarked entry ids for [archive_id] +// ----------------------------------------------------------------------------- +vector library::bookmarkedEntries(int64_t archive_id) +{ + vector entry_ids; + + if (auto sql = db::cacheQuery( + "archive_all_bookmarks", "SELECT entry_id FROM archive_bookmark WHERE archive_id = ?")) + { + sql->bind(1, archive_id); + while (sql->executeStep()) + entry_ids.push_back(sql->getColumn(0).getInt64()); + sql->reset(); + } + + return entry_ids; +} + +// ----------------------------------------------------------------------------- +// Adds a bookmarked entry to the library +// ----------------------------------------------------------------------------- +void library::addBookmark(int64_t archive_id, int64_t entry_id) +{ + if (archive_id < 0 || entry_id < 0) + return; + + if (auto sql = db::cacheQuery("insert_archive_bookmark", insert_archive_bookmark, true)) + { + sql->bind(1, archive_id); + sql->bind(2, entry_id); + sql->exec(); + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Removes a bookmarked entry from the library +// ----------------------------------------------------------------------------- +void library::removeBookmark(int64_t archive_id, int64_t entry_id) +{ + if (archive_id < 0 || entry_id < 0) + return; + + if (auto sql = db::cacheQuery( + "delete_archive_bookmark", "DELETE FROM archive_bookmark WHERE archive_id = ? AND entry_id = ?", true)) + { + sql->bind(1, archive_id); + sql->bind(2, entry_id); + sql->exec(); + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Removes all bookmarked entries for [archive_id] in the library +// ----------------------------------------------------------------------------- +void library::removeArchiveBookmarks(int64_t archive_id) +{ + db::connectionRW()->exec(fmt::format("DELETE FROM archive_bookmark WHERE archive_id = {}", archive_id)); +} + +// ----------------------------------------------------------------------------- +// Writes multiple bookmarked entries to the library +// ----------------------------------------------------------------------------- +void library::writeArchiveBookmarks(int64_t archive_id, const vector& entry_ids) +{ + auto connection = db::connectionRW(); + + // Delete existing bookmarks in library first + removeArchiveBookmarks(archive_id); + + // Insert bookmark rows + if (auto sql = db::cacheQuery("insert_archive_bookmark", insert_archive_bookmark, true)) + { + db::Transaction transaction{ connection, false }; + transaction.beginIfNoActiveTransaction(); + + for (const auto& entry_id : entry_ids) + { + sql->bind(1, archive_id); + sql->bind(2, entry_id); + sql->exec(); + sql->reset(); + } + + transaction.commit(); + } +} + +// ----------------------------------------------------------------------------- +// Attempts to find the EntryType id of the given [entry] by finding an entry +// in the library with the exact same name and data +// ----------------------------------------------------------------------------- +string library::findEntryTypeId(const ArchiveEntry& entry) +{ + if (entry.size() == 0) + return "marker"; + + string format_id; + + if (auto sql = db::cacheQuery( + "find_entry_type_id", "SELECT type_id FROM archive_entry WHERE name = ? AND hash = ?")) + { + sql->bind(1, entry.name()); + sql->bind(2, entry.hash()); + + if (sql->executeStep()) + format_id = sql->getColumn(0).getString(); + + sql->reset(); + } + + return format_id; +} + +// ----------------------------------------------------------------------------- +// Reads the pre-3.3.0 recent_files section in slade3.cfg +// ----------------------------------------------------------------------------- +void library::readPre330RecentFiles(Tokenizer& tz) +{ + while (!tz.checkOrEnd("}")) + { + // Read recent file path + auto path = wxString::FromUTF8(tz.current().text.c_str()).ToStdString(); + tz.adv(); + + // Check the path is valid + if (!(fileutil::fileExists(path) || fileutil::dirExists(path))) + continue; + + recent_files.insert(recent_files.begin(), path); + } + + tz.adv(); // Skip ending } +} + +// ----------------------------------------------------------------------------- +// Returns the library signals struct +// ----------------------------------------------------------------------------- +Signals& library::signals() +{ + return lib_signals; +} + + + +#include "General/Console.h" + +CONSOLE_COMMAND(lib_cleanup, 0, true) +{ + log::console("Removing missing archives..."); + library::removeMissingArchives(); + + log::console("Library cleanup complete"); +} + +CONSOLE_COMMAND(lib_scan, 1, true) +{ + if (args[0] == "stop") + { + // Stop scan requested + lib_scan_running = false; + log::console("Library scan stop requested, will stop after the current archive is finished scanning"); + return; + } + + vector ignore_ext; //{ "zip" }; + + // Start scan in background thread + std::thread scan_thread( + [ignore_ext, args] + { + // Create+Register database connection context for thread + db::Context ctx{ db::programDatabasePath() }; + db::registerThreadContext(ctx); + + bool rebuild = false; + if (args.size() >= 2 && args[1] == "rebuild") + rebuild = true; + + library::scanArchivesInDir(args[0], ignore_ext, rebuild); + + db::deregisterThreadContexts(); + }); + scan_thread.detach(); +} diff --git a/src/Library/Library.h b/src/Library/Library.h new file mode 100644 index 000000000..1cc9d3398 --- /dev/null +++ b/src/Library/Library.h @@ -0,0 +1,49 @@ +#pragma once + +namespace slade +{ +class Archive; +class ArchiveEntry; +class Tokenizer; + +namespace library +{ + struct Signals + { + sigslot::signal archive_file_updated; + sigslot::signal archive_file_inserted; + sigslot::signal archive_file_deleted; + }; + + // General + void init(); + Signals& signals(); + + // Archives + int64_t readArchiveInfo(const Archive& archive); + void setArchiveLastOpenedTime(int64_t archive_id, time_t last_opened); + int64_t writeArchiveInfo(const Archive& archive); + void writeArchiveEntryInfo(const Archive& archive); + void writeArchiveMapInfo(const Archive& archive); + void removeMissingArchives(); + vector recentFiles(unsigned count = 20); + + // Archive Dir Scan + void scanArchivesInDir(string_view path, const vector& ignore_ext, bool rebuild = false); + void stopArchiveDirScan(); + bool archiveDirScanRunning(); + + // Bookmarks + vector bookmarkedEntries(int64_t archive_id); + void addBookmark(int64_t archive_id, int64_t entry_id); + void removeBookmark(int64_t archive_id, int64_t entry_id); + void removeArchiveBookmarks(int64_t archive_id); + void writeArchiveBookmarks(int64_t archive_id, const vector& entry_ids); + + // Entries + string findEntryTypeId(const slade::ArchiveEntry& entry); + + // Recent Files (for pre-3.3.0 compatibility, remove in 3.4.0) + void readPre330RecentFiles(Tokenizer& tz); +} // namespace library +} // namespace slade diff --git a/src/Library/UI/LibraryPanel.cpp b/src/Library/UI/LibraryPanel.cpp new file mode 100644 index 000000000..44dc26794 --- /dev/null +++ b/src/Library/UI/LibraryPanel.cpp @@ -0,0 +1,595 @@ + +#include "Main.h" +#include "LibraryPanel.h" +#include "App.h" +#include "Archive/Archive.h" +#include "Archive/ArchiveFormat.h" +#include "Archive/ArchiveManager.h" +#include "Database/Context.h" +#include "Database/Database.h" +#include "General/Misc.h" +#include "General/SAction.h" +#include "General/UI.h" +#include "Graphics/Icons.h" +#include "Library/ArchiveFile.h" +#include "Library/Library.h" +#include "MainEditor/MainEditor.h" +#include "UI/Dialogs/RunDialog.h" +#include "UI/SToolBar/SToolBar.h" +#include "UI/State.h" +#include "UI/WxUtils.h" +#include "Utility/DateTime.h" +#include "Utility/StringUtils.h" +#include + +using namespace slade; +using namespace ui; +using namespace library; + + +namespace +{ +icons::IconCache icon_cache; +} + + +namespace +{ +template int compare(const T& left, const T& right) +{ + if (left == right) + return 0; + + return left < right ? -1 : 1; +} + +void openArchive(const LibraryViewModel::LibraryListRow* row) +{ + // Check if it's an archive within another archive + if (row->parent_id >= 0) + { + // Open parent archive + ArchiveFileRow parent_row{ db::global(), row->parent_id }; + maineditor::openArchiveFile(parent_row.path); + + // Now open the archive's entry within the parent + auto parent_archive = app::archiveManager().getArchive(parent_row.path); + if (auto entry = parent_archive->entryAtPath(strutil::replace(row->path, parent_row.path, {}))) + maineditor::openEntry(entry); + + return; + } + + maineditor::openArchiveFile(row->path); +} +} // namespace + +LibraryViewModel::LibraryViewModel() +{ + loadRows(); + Cleared(); + + // Connect to library signals ---------------------------------------------- + auto& signals = library::signals(); + + // Archive updated + signal_connections_ += signals.archive_file_updated.connect( + [this](int64_t id) + { + bool changed = false; + for (auto& row : rows_) + if (row.id == id) + { + row = LibraryListRow{ db::global(), id }; + ItemChanged(wxDataViewItem{ &row }); + changed = true; + } + + if (changed) + Resort(); + }); + + // Archive added + signal_connections_ += signals.archive_file_inserted.connect( + [this](int64_t id) + { + rows_.emplace_back(database::global(), id); + ItemAdded({}, wxDataViewItem{ &rows_.back() }); + Resort(); + }); + + // Archive deleted + signal_connections_ += signals.archive_file_deleted.connect( + [this](int64_t id) + { + for (auto i = 0; i < rows_.size(); ++i) + if (rows_[i].id == id) + { + ItemDeleted({}, wxDataViewItem{ &rows_[i] }); + rows_.erase(rows_.begin() + i); + Resort(); + break; + } + }); +} + +LibraryViewModel::LibraryListRow::LibraryListRow(database::Context& db, int64_t id) +{ + if (auto sql = db.cacheQuery("get_library_list", "SELECT * FROM archive_library_list WHERE id = ?")) + { + sql->bind(1, id); + + if (sql->executeStep()) + { + this->id = id; + path = sql->getColumn(1).getString(); + size = sql->getColumn(2).getUInt(); + format_id = sql->getColumn(3).getString(); + last_opened = sql->getColumn(4).getInt64(); + last_modified = sql->getColumn(5).getInt64(); + parent_id = sql->getColumn(6).getInt64(); + entry_count = sql->getColumn(7).getUInt(); + map_count = sql->getColumn(8).getUInt(); + } + + sql->reset(); + } +} + +wxDataViewItem LibraryViewModel::itemForArchiveId(int64_t id) const +{ + for (auto& row : rows_) + if (row.id == id) + return wxDataViewItem{ &row }; + + return {}; +} + +void LibraryViewModel::setFilter(string_view filter) +{ + filter_ = filter; + + Cleared(); +} + +wxString LibraryViewModel::GetColumnType(unsigned col) const +{ + switch (static_cast(col)) + { + case Column::Name: return "wxDataViewIconText"; + default: return "string"; + } +} + +void LibraryViewModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned col) const +{ + auto* row = static_cast(item.GetID()); + if (!row) + return; + + switch (static_cast(col)) + { + case Column::Name: + { + // Determine icon + string icon = "archive"; + if (row->format_id == "wad") + icon = "wad"; + else if (row->format_id == "zip") + icon = "zip"; + else if (row->format_id == "folder") + icon = "folder"; + + // Find icon in cache + if (!icon_cache.isCached(icon)) + { + // Not found, add to cache + constexpr auto pad = Point2i{ 1, 1 }; + icon_cache.cacheIcon(icons::Type::Entry, icon, 16, pad); + } + + auto fn = wxutil::strFromView(strutil::Path::fileNameOf(row->path)); + + variant << wxDataViewIconText(fn, icon_cache.icons[icon]); + + break; + } + case Column::Path: variant = wxutil::strFromView(strutil::Path::pathOf(row->path, false)); break; + case Column::Size: variant = misc::sizeAsString(row->size); break; + case Column::Type: + { + auto fn_ext = strutil::Path::extensionOf(row->path); + auto desc = archive::formatInfoFromId(row->format_id); + + variant = desc.name; + + for (const auto& ext : desc.extensions) + if (strutil::equalCI(fn_ext, ext.first)) + variant = ext.second; + + break; + } + case Column::LastOpened: + if (row->last_opened == 0) + variant = "Never"; + else + variant = datetime::toString(row->last_opened, datetime::Format::Local); + break; + case Column::FileModified: + if (row->last_modified == 0) + variant = "Unknown"; + else + variant = datetime::toString(row->last_modified, datetime::Format::Local); + break; + case Column::EntryCount: variant = wxString::Format("%d", row->entry_count); break; + case Column::MapCount: variant = row->map_count > 0 ? wxString ::Format("%d", row->map_count) : ""; break; + default: break; + } +} + +bool LibraryViewModel::GetAttr(const wxDataViewItem& item, unsigned col, wxDataViewItemAttr& attr) const +{ + return wxDataViewModel::GetAttr(item, col, attr); +} + +bool LibraryViewModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned col) +{ + return false; +} + +wxDataViewItem LibraryViewModel::GetParent(const wxDataViewItem& item) const +{ + return {}; +} + +bool LibraryViewModel::IsContainer(const wxDataViewItem& item) const +{ + return !item.IsOk(); +} + +unsigned LibraryViewModel::GetChildren(const wxDataViewItem& item, wxDataViewItemArray& children) const +{ + if (!item.IsOk()) + { + // Root item + for (auto& row : rows_) + if (matchesFilter(row)) + children.Add(wxDataViewItem{ &row }); + + return children.size(); + } + + return 0; +} + +int LibraryViewModel::Compare(const wxDataViewItem& item1, const wxDataViewItem& item2, unsigned column, bool ascending) + const +{ + auto row1 = static_cast(item1.GetID()); + auto row2 = static_cast(item2.GetID()); + if (!row1 || !row2) + return 0; + + auto col = static_cast(column); + + // Size column + if (col == Column::Size) + return ascending ? compare(row1->size, row2->size) : compare(row2->size, row1->size); + + // Last Opened column + if (col == Column::LastOpened) + return ascending ? compare(row1->last_opened, row2->last_opened) : + compare(row2->last_opened, row1->last_opened); + + // File Modified column + if (col == Column::FileModified) + return ascending ? compare(row1->last_modified, row2->last_modified) : + compare(row2->last_modified, row1->last_modified); + + // Entry Count column + if (col == Column::EntryCount) + return ascending ? compare(row1->entry_count, row2->entry_count) : + compare(row2->entry_count, row1->entry_count); + + // Map Count column + if (col == Column::MapCount) + return ascending ? compare(row1->map_count, row2->map_count) : compare(row2->map_count, row1->map_count); + + // Default compare + return wxDataViewModel::Compare(item1, item2, column, ascending); +} + +void LibraryViewModel::loadRows() const +{ + rows_.clear(); + + if (auto sql = db::cacheQuery("library_list", "SELECT * FROM archive_library_list")) + { + LibraryListRow row; + while (sql->executeStep()) + { + row.id = sql->getColumn(0).getInt64(); + row.path = sql->getColumn(1).getString(); + row.size = sql->getColumn(2).getUInt(); + row.format_id = sql->getColumn(3).getString(); + row.last_opened = sql->getColumn(4).getInt64(); + row.last_modified = sql->getColumn(5).getInt64(); + row.parent_id = sql->getColumn(6).getInt64(); + row.entry_count = sql->getColumn(7).getUInt(); + row.map_count = sql->getColumn(8).getUInt(); + + rows_.push_back(row); + } + + sql->reset(); + } +} + +bool LibraryViewModel::matchesFilter(const LibraryListRow& row) const +{ + // Check for filename match if needed + if (!filter_.empty()) + return strutil::matchesCI(strutil::Path::fileNameOf(row.path), fmt::format("*{}*", filter_)); + + return true; +} + +LibraryPanel::LibraryPanel(wxWindow* parent) : wxPanel{ parent } +{ + setup(); +} + +void LibraryPanel::setup() +{ + auto sizer = new wxBoxSizer(wxVERTICAL); + SetSizer(sizer); + + // Toolbar + setupToolbar(); + sizer->Add(toolbar_, wxutil::sfWithBorder(0, wxLEFT | wxRIGHT | wxTOP).Expand()); + sizer->AddSpacer(px(Size::Pad)); + + // Archive list + list_archives_ = new SDataViewCtrl{ this, wxDV_MULTIPLE }; + sizer->Add(list_archives_, wxutil::sfWithBorder(1, wxLEFT | wxRIGHT | wxBOTTOM).Expand()); + + // Init archive list + model_library_ = new LibraryViewModel(); + list_archives_->AssociateModel(model_library_); + model_library_->DecRef(); + setupListColumns(); + + bindEvents(); +} + +void LibraryPanel::setupToolbar() +{ + toolbar_ = new SToolBar(this); + + // Actions + toolbar_->addActionGroup("_Library", { "alib_open", "alib_run", "alib_remove" }); + + // Filter + auto tbg_filter = new SToolBarGroup(toolbar_, "Filter"); + text_filter_ = new wxTextCtrl(tbg_filter, -1); + tbg_filter->addCustomControl(new wxStaticText(tbg_filter, -1, "Filter:")); + tbg_filter->addCustomControl(text_filter_); + toolbar_->addGroup(tbg_filter, true); +} + +bool LibraryPanel::handleAction(string_view id) +{ + // Open + if (id == "alib_open") + { + wxDataViewItemArray selection; + list_archives_->GetSelections(selection); + + for (const auto& item : selection) + if (auto row = model_library_->rowForItem(item)) + openArchive(row); + + return true; + } + + // Remove + if (id == "alib_remove") + { + wxDataViewItemArray selection; + list_archives_->GetSelections(selection); + + vector to_remove; + for (const auto& item : selection) + if (auto row = model_library_->rowForItem(item)) + to_remove.push_back(row->id); + + for (auto archive_id : to_remove) + library::removeArchiveFile(archive_id); + + return true; + } + + // Run + if (id == "alib_run") + { + wxDataViewItemArray selection; + list_archives_->GetSelections(selection); + + string path; + int64_t id = -1; + for (const auto& item : selection) + if (auto row = model_library_->rowForItem(item)) + { + path = row->path; + id = row->id; + break; + } + + RunDialog dlg{ this, id }; + if (dlg.ShowModal() == wxID_OK) + dlg.run(RunDialog::Config{ path }, id); + + return true; + } + + return false; +} + +void LibraryPanel::bindEvents() +{ + // Open archive if activated + list_archives_->Bind( + wxEVT_DATAVIEW_ITEM_ACTIVATED, + [this](wxDataViewEvent& e) + { + if (auto row = model_library_->rowForItem(e.GetItem())) + openArchive(row); + }); + + // Context menu + list_archives_->Bind( + wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, + [this](wxDataViewEvent& e) + { + wxMenu context; + SAction::fromId("alib_open")->addToMenu(&context); + SAction::fromId("alib_run")->addToMenu(&context); + SAction::fromId("alib_remove")->addToMenu(&context); + list_archives_->PopupMenu(&context); + }); + + // Header right click + list_archives_->Bind( + wxEVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK, + [this](wxDataViewEvent& e) + { + using Column = LibraryViewModel::Column; + + // Popup context menu + wxMenu context; + context.Append(static_cast(Column::__Count), "Reset Sorting"); + context.AppendSeparator(); + list_archives_->appendColumnToggleItem(context, static_cast(Column::Path)); + list_archives_->appendColumnToggleItem(context, static_cast(Column::Size)); + list_archives_->appendColumnToggleItem(context, static_cast(Column::Type)); + list_archives_->appendColumnToggleItem(context, static_cast(Column::LastOpened)); + list_archives_->appendColumnToggleItem(context, static_cast(Column::FileModified)); + list_archives_->appendColumnToggleItem(context, static_cast(Column::EntryCount)); + list_archives_->appendColumnToggleItem(context, static_cast(Column::MapCount)); + list_archives_->PopupMenu(&context); + e.Skip(); + }); + + // Header context menu + list_archives_->Bind( + wxEVT_MENU, + [this](wxCommandEvent& e) + { + using Column = LibraryViewModel::Column; + + string toggle_column; + switch (static_cast(e.GetId())) + { + case Column::Name: break; + case Column::Path: toggle_column = "LibraryPanelPathVisible"; break; + case Column::Size: toggle_column = "LibraryPanelSizeVisible"; break; + case Column::Type: toggle_column = "LibraryPanelTypeVisible"; break; + case Column::LastOpened: toggle_column = "LibraryPanelLastOpenedVisible"; break; + case Column::FileModified: toggle_column = "LibraryPanelFileModifiedVisible"; break; + case Column::EntryCount: toggle_column = "LibraryPanelEntryCountVisible"; break; + case Column::MapCount: toggle_column = "LibraryPanelMapCountVisible"; break; + case Column::__Count: list_archives_->resetSorting(); break; + default: e.Skip(); break; + } + + if (!toggle_column.empty()) + { + list_archives_->toggleColumnVisibility(e.GetId(), toggle_column); + updateColumnWidths(); + } + }); + + // List column resized + list_archives_->Bind( + EVT_SDVC_COLUMN_RESIZED, + [this](wxDataViewEvent& e) + { + using Column = LibraryViewModel::Column; + auto col = static_cast(e.GetColumn()); + auto width = e.GetDataViewColumn()->GetWidth(); + + switch (col) + { + case Column::Name: saveStateInt("LibraryPanelFilenameWidth", width); break; + case Column::Path: saveStateInt("LibraryPanelPathWidth", width); break; + case Column::Size: saveStateInt("LibraryPanelSizeWidth", width); break; + case Column::Type: saveStateInt("LibraryPanelTypeWidth", width); break; + case Column::LastOpened: saveStateInt("LibraryPanelLastOpenedWidth", width); break; + case Column::FileModified: saveStateInt("LibraryPanelFileModifiedWidth", width); break; + case Column::EntryCount: saveStateInt("LibraryPanelEntryCountWidth", width); break; + case Column::MapCount: saveStateInt("LibraryPanelMapCountWidth", width); break; + default: break; + } + }); + + // Filter changed + text_filter_->Bind( + wxEVT_TEXT, + [this](wxCommandEvent& e) { model_library_->setFilter(wxutil::strToView(text_filter_->GetValue())); }); +} + +void LibraryPanel::setupListColumns() const +{ + using Column = LibraryViewModel::Column; + + // Search by filename column + list_archives_->setSearchColumn(static_cast(Column::Name)); + + // Filename column is fixed + list_archives_->AppendIconTextColumn( + "Filename", + static_cast(Column::Name), + wxDATAVIEW_CELL_INERT, + getStateInt("LibraryPanelFilenameWidth"), + wxALIGN_NOT, + wxDATAVIEW_COL_SORTABLE | wxDATAVIEW_COL_RESIZABLE); + + // Add other columns + appendTextColumn(Column::Path, "Path", "Path"); + appendTextColumn(Column::Size, "Size", "Size"); + appendTextColumn(Column::Type, "Type", "Type"); + appendTextColumn(Column::LastOpened, "Last Opened", "LastOpened"); + appendTextColumn(Column::FileModified, "File Modified", "FileModified"); + appendTextColumn(Column::EntryCount, "# Entries", "EntryCount"); + appendTextColumn(Column::MapCount, "# Maps", "MapCount"); +} + +void LibraryPanel::updateColumnWidths() +{ + using Column = LibraryViewModel::Column; + + Freeze(); + list_archives_->setColumnWidth(modelColumn(Column::Name), getStateInt("LibraryPanelFilenameWidth")); + list_archives_->setColumnWidth(modelColumn(Column::Path), getStateInt("LibraryPanelPathWidth")); + list_archives_->setColumnWidth(modelColumn(Column::Size), getStateInt("LibraryPanelSizeWidth")); + list_archives_->setColumnWidth(modelColumn(Column::Type), getStateInt("LibraryPanelTypeWidth")); + list_archives_->setColumnWidth(modelColumn(Column::LastOpened), getStateInt("LibraryPanelLastOpenedWidth")); + list_archives_->setColumnWidth(modelColumn(Column::FileModified), getStateInt("LibraryPanelFileModifiedWidth")); + list_archives_->setColumnWidth(modelColumn(Column::EntryCount), getStateInt("LibraryPanelEntryCountWidth")); + list_archives_->setColumnWidth(modelColumn(Column::MapCount), getStateInt("LibraryPanelMapCountWidth")); + Thaw(); +} + +void LibraryPanel::appendTextColumn(LibraryViewModel::Column column, string_view title, string_view id) const +{ + static auto colstyle_visible = wxDATAVIEW_COL_SORTABLE | wxDATAVIEW_COL_RESIZABLE; + static auto colstyle_hidden = colstyle_visible | wxDATAVIEW_COL_HIDDEN; + + list_archives_->AppendTextColumn( + wxutil::strFromView(title), + static_cast(column), + wxDATAVIEW_CELL_INERT, + getStateInt(fmt::format("LibraryPanel{}Width", id)), + wxALIGN_NOT, + getStateBool(fmt::format("LibraryPanel{}Visible", id)) ? colstyle_visible : colstyle_hidden); +} diff --git a/src/Library/UI/LibraryPanel.h b/src/Library/UI/LibraryPanel.h new file mode 100644 index 000000000..8ccf768cb --- /dev/null +++ b/src/Library/UI/LibraryPanel.h @@ -0,0 +1,109 @@ +#pragma once + +#include "General/SActionHandler.h" +#include "General/Sigslot.h" +#include "Library/ArchiveFile.h" +#include "UI/Lists/SDataViewCtrl.h" + +namespace slade +{ +class SToolBar; + +namespace ui +{ + class LibraryViewModel : public wxDataViewModel + { + public: + LibraryViewModel(); + ~LibraryViewModel() override = default; + + enum class Column + { + Name = 0, + Path, + Size, + Type, + LastOpened, + FileModified, + EntryCount, + MapCount, + + __Count + }; + + struct LibraryListRow + { + int64_t id = -1; + string path; + unsigned size = 0; + string format_id; + time_t last_opened = 0; + time_t last_modified = 0; + int64_t parent_id = -1; + unsigned entry_count = 0; + unsigned map_count = 0; + + LibraryListRow() = default; + LibraryListRow(database::Context& db, int64_t id); + }; + + wxDataViewItem itemForArchiveId(int64_t id) const; + LibraryListRow* rowForItem(const wxDataViewItem& item) const + { + return static_cast(item.GetID()); + } + + void setFilter(string_view filter); + + private: + mutable vector rows_; + ScopedConnectionList signal_connections_; + string filter_; + + // wxDataViewModel + unsigned int GetColumnCount() const override { return static_cast(Column::__Count); } + wxString GetColumnType(unsigned int col) const override; + void GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const override; + bool GetAttr(const wxDataViewItem& item, unsigned int col, wxDataViewItemAttr& attr) const override; + bool SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) override; + wxDataViewItem GetParent(const wxDataViewItem& item) const override; + bool IsContainer(const wxDataViewItem& item) const override; + unsigned int GetChildren(const wxDataViewItem& item, wxDataViewItemArray& children) const override; + bool IsListModel() const override { return true; } + bool HasDefaultCompare() const override { return true; } + int Compare(const wxDataViewItem& item1, const wxDataViewItem& item2, unsigned int column, bool ascending) + const override; + + void loadRows() const; + bool matchesFilter(const LibraryListRow& row) const; + }; + + class LibraryPanel : public wxPanel, SActionHandler + { + public: + LibraryPanel(wxWindow* parent); + ~LibraryPanel() override = default; + + // SAction handler + bool handleAction(string_view id) override; + + private: + SDataViewCtrl* list_archives_ = nullptr; + LibraryViewModel* model_library_ = nullptr; + SToolBar* toolbar_ = nullptr; + wxTextCtrl* text_filter_ = nullptr; + + void setup(); + void setupToolbar(); + void bindEvents(); + void setupListColumns() const; + void updateColumnWidths(); + void appendTextColumn(LibraryViewModel::Column column, string_view title, string_view id) const; + + wxDataViewColumn* modelColumn(LibraryViewModel::Column column) const + { + return list_archives_->GetColumn(list_archives_->modelColumnIndex(static_cast(column))); + } + }; +} // namespace ui +} // namespace slade diff --git a/src/MainEditor/ArchiveOperations.cpp b/src/MainEditor/ArchiveOperations.cpp index 1283fe2ea..2091f2ca4 100644 --- a/src/MainEditor/ArchiveOperations.cpp +++ b/src/MainEditor/ArchiveOperations.cpp @@ -156,9 +156,6 @@ bool archiveoperations::saveAs(Archive& archive) return false; } - // Add recent file - app::archiveManager().addRecentFile(filename); - return true; } @@ -2368,7 +2365,7 @@ size_t archiveoperations::replaceThings(Archive* archive, int oldtype, int newty { // Attempt to open entry as wad archive auto temp_archive = std::make_shared(ArchiveFormat::Wad); - if (temp_archive->open(m_head->data())) + if (temp_archive->open(m_head->data(), true)) { achanged = archiveoperations::replaceThings(temp_archive.get(), oldtype, newtype); MemChunk mc; @@ -2739,7 +2736,7 @@ size_t archiveoperations::replaceSpecials( { // Attempt to open entry as wad archive auto temp_archive = std::make_unique(ArchiveFormat::Wad); - if (temp_archive->open(m_head.get())) + if (temp_archive->open(m_head.get(), true)) { achanged = archiveoperations::replaceSpecials( temp_archive.get(), @@ -3188,7 +3185,7 @@ size_t archiveoperations::replaceTextures( { // Attempt to open entry as wad archive auto temp_archive = std::make_unique(ArchiveFormat::Wad); - if (temp_archive->open(m_head.get())) + if (temp_archive->open(m_head.get(), true)) { achanged = archiveoperations::replaceTextures( temp_archive.get(), oldtex, newtex, floor, ceiling, lower, middle, upper); diff --git a/src/MainEditor/MainEditor.cpp b/src/MainEditor/MainEditor.cpp index cd33f4035..49975006d 100644 --- a/src/MainEditor/MainEditor.cpp +++ b/src/MainEditor/MainEditor.cpp @@ -38,6 +38,7 @@ #include "UI/ArchivePanel.h" #include "UI/Controls/PaletteChooser.h" #include "UI/MainWindow.h" +#include "UI/WxUtils.h" using namespace slade; @@ -126,6 +127,14 @@ void maineditor::openMapEditor(Archive* archive) mapeditor::chooseMap(archive); } +// ----------------------------------------------------------------------------- +// Opens the archive file at [filename] +// ----------------------------------------------------------------------------- +void maineditor::openArchiveFile(string_view filename) +{ + main_window->archiveManagerPanel()->openFile(wxutil::strFromView(filename)); +} + // ----------------------------------------------------------------------------- // Shows the tab for [archive], opening a new tab for it if needed // ----------------------------------------------------------------------------- diff --git a/src/MainEditor/MainEditor.h b/src/MainEditor/MainEditor.h index 43839d8e4..1071e321c 100644 --- a/src/MainEditor/MainEditor.h +++ b/src/MainEditor/MainEditor.h @@ -33,6 +33,7 @@ namespace maineditor void openTextureEditor(const Archive* archive, const ArchiveEntry* entry = nullptr); void openMapEditor(Archive* archive); + void openArchiveFile(string_view filename); void openArchiveTab(const Archive* archive); void openEntry(ArchiveEntry* entry); bool saveArchiveAs(Archive* archive); diff --git a/src/MainEditor/UI/ArchiveManagerPanel.cpp b/src/MainEditor/UI/ArchiveManagerPanel.cpp index ab22fd3a4..24592e10a 100644 --- a/src/MainEditor/UI/ArchiveManagerPanel.cpp +++ b/src/MainEditor/UI/ArchiveManagerPanel.cpp @@ -44,6 +44,7 @@ #include "General/SAction.h" #include "General/UI.h" #include "Graphics/Icons.h" +#include "Library/Library.h" #include "MainEditor/ArchiveOperations.h" #include "MainEditor/MainEditor.h" #include "MainEditor/UI/MainWindow.h" @@ -54,7 +55,9 @@ #include "UI/Dialogs/DirArchiveUpdateDialog.h" #include "UI/Dialogs/NewArchiveDiaog.h" #include "UI/Lists/ListView.h" +#include "UI/State.h" #include "UI/WxUtils.h" +#include "Utility/SFileDialog.h" #include "Utility/StringUtils.h" using namespace slade; @@ -66,7 +69,6 @@ using namespace slade; // // ----------------------------------------------------------------------------- CVAR(Bool, close_archive_with_tab, true, CVar::Flag::Save) -CVAR(Int, am_current_tab, 0, CVar::Flag::Save) CVAR(Bool, am_file_browser_tab, false, CVar::Flag::Save) CVAR(Int, dir_archive_change_action, 2, CVar::Flag::Save) // 0=always ignore, 1=always apply, 2+=ask @@ -76,7 +78,6 @@ CVAR(Int, dir_archive_change_action, 2, CVar::Flag::Save) // 0=always ignore, 1= // External Variables // // ----------------------------------------------------------------------------- -EXTERN_CVAR(String, dir_last) EXTERN_CVAR(Int, autosave_entry_changes) @@ -386,7 +387,7 @@ ArchiveManagerPanel::ArchiveManagerPanel(wxWindow* parent, STabCtrl* nb_archives } // Set current tab - stc_tabs_->SetSelection(am_current_tab); + stc_tabs_->SetSelection(ui::getStateInt("ArchiveManagerCurrentTab")); // Bind events list_archives_->Bind(wxEVT_LIST_ITEM_SELECTED, &ArchiveManagerPanel::onListArchivesChanged, this); @@ -401,7 +402,8 @@ ArchiveManagerPanel::ArchiveManagerPanel(wxWindow* parent, STabCtrl* nb_archives stc_archives_->Bind(wxEVT_AUINOTEBOOK_PAGE_CLOSE, &ArchiveManagerPanel::onArchiveTabClose, this); stc_archives_->Bind(wxEVT_AUINOTEBOOK_PAGE_CLOSED, &ArchiveManagerPanel::onArchiveTabClosed, this); stc_tabs_->Bind( - wxEVT_AUINOTEBOOK_PAGE_CHANGED, [&](wxAuiNotebookEvent&) { am_current_tab = stc_tabs_->GetSelection(); }); + wxEVT_AUINOTEBOOK_PAGE_CHANGED, + [&](wxAuiNotebookEvent&) { ui::saveStateInt("ArchiveManagerCurrentTab", stc_tabs_->GetSelection()); }); Bind(wxEVT_COMMAND_DIRARCHIVECHECK_COMPLETED, &ArchiveManagerPanel::onDirArchiveCheckCompleted, this); connectSignals(); @@ -474,7 +476,7 @@ void ArchiveManagerPanel::layoutHorizontal() // ----------------------------------------------------------------------------- // Clears and rebuilds the recent file list in the menu and the tab // ----------------------------------------------------------------------------- -void ArchiveManagerPanel::refreshRecentFileList() const +void ArchiveManagerPanel::refreshRecentFileList() { // Clear the list list_recent_->ClearAll(); @@ -494,7 +496,8 @@ void ArchiveManagerPanel::refreshRecentFileList() const // Add each recent archive (same logic as the recent files submenu) list_recent_->enableSizeUpdate(false); - for (unsigned a = 0; a < app::archiveManager().numRecentFiles(); a++) + recent_file_paths_ = wxutil::arrayStringStd(library::recentFiles()); + for (unsigned a = 0; a < recent_file_paths_.size(); a++) { list_recent_->addItem(a, wxEmptyString); updateRecentListItem(a); @@ -502,17 +505,17 @@ void ArchiveManagerPanel::refreshRecentFileList() const if (a < 8) { // Get path and determine icon - auto fn = app::archiveManager().recentFile(a); + auto fn = recent_file_paths_[a]; string icon = "archive"; - if (strutil::endsWith(fn, ".wad")) + if (fn.EndsWith(".wad")) icon = "wad"; - else if (strutil::endsWith(fn, ".zip") || strutil::endsWith(fn, ".pk3") || strutil::endsWith(fn, ".pke")) + else if (fn.EndsWith(".zip") || fn.EndsWith(".pk3") || fn.EndsWith(".pke")) icon = "zip"; else if (wxDirExists(fn)) icon = "folder"; // Create and add menu item - a_recent->addToMenu(menu_recent_, 0, fn, icon, a); + a_recent->addToMenu(menu_recent_, 0, wxutil::strToView(fn), icon, a); // wxMenuItem* mi = new wxMenuItem(menu_recent, id_recent_start + a, fn); // mi->SetBitmap(Icons::getIcon(Icons::ENTRY, icon)); // menu_recent->Append(mi); @@ -610,8 +613,8 @@ void ArchiveManagerPanel::updateOpenListItem(int index) const void ArchiveManagerPanel::updateRecentListItem(int index) const { // Get path as wxFileName for processing - wxString path = app::archiveManager().recentFile(index); - wxFileName fn(path); + const auto& path = recent_file_paths_[index]; + wxFileName fn(path); // Set item name list_recent_->setItemText(index, 0, fn.GetFullName()); @@ -1406,7 +1409,7 @@ bool ArchiveManagerPanel::closeAll() // ----------------------------------------------------------------------------- // Saves all currently open archives // ----------------------------------------------------------------------------- -void ArchiveManagerPanel::saveAll() const +void ArchiveManagerPanel::saveAll() { // Go through all open archives for (int a = 0; a < app::archiveManager().numArchives(); a++) @@ -1427,29 +1430,17 @@ void ArchiveManagerPanel::saveAll() const { // If the archive is newly created, do Save As instead - // Popup file save dialog - wxString formats = archive->fileExtensionString(); - wxString filename = wxFileSelector( - "Save Archive " + archive->filename(false) + " As", - dir_last, - "", - wxEmptyString, - formats, - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - // Check a filename was selected - if (!filename.empty()) + // Popup file save dialog and check a filename was selected + if (auto filename = filedialog::saveFile( + fmt::format("Save Archive {} As", archive->filename(false)), archive->fileExtensionString(), this); + !filename.empty()) { // Save the archive - if (!archive->save(filename.ToStdString())) + if (!archive->save(filename)) { // If there was an error pop up a message box wxMessageBox(wxString::Format("Error: %s", global::error), "Error", wxICON_ERROR); } - - // Save 'dir_last' - wxFileName fn(filename); - dir_last = wxutil::strToView(fn.GetPath(true)); } } } @@ -1831,7 +1822,7 @@ void ArchiveManagerPanel::openSelection() const // Get the list of selected archives vector selected_archives; for (int index : selection) - selected_archives.emplace_back(app::archiveManager().recentFile(index)); + selected_archives.push_back(recent_file_paths_[index]); // Open all selected archives for (const auto& selected_archive : selected_archives) @@ -1852,8 +1843,8 @@ void ArchiveManagerPanel::removeSelection() const // Remove selected recent files (starting from the last and going backward, // because the list reorders itself whenever an item is removed) - for (unsigned a = selection.size(); a > 0; --a) - app::archiveManager().removeRecentFile(app::archiveManager().recentFile(selection[a - 1])); + // for (unsigned a = selection.size(); a > 0; --a) + // app::archiveManager().removeRecentFile(app::archiveManager().recentFile(selection[a - 1])); } // ----------------------------------------------------------------------------- @@ -1886,59 +1877,23 @@ bool ArchiveManagerPanel::handleAction(string_view id) else if (id == "aman_open") { // Create extensions string - wxString extensions = app::archiveManager().getArchiveExtensionsString(); + auto extensions = app::archiveManager().getArchiveExtensionsString(); extensions += "|All Files (*.*)|*.*"; // Open a file browser dialog that allows multiple selection - // and filters by wad, zip and pk3 file extensions - wxFileDialog dialog_open( - this, - "Choose file(s) to open", - dir_last, - wxEmptyString, - extensions, - wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST, - wxDefaultPosition); - - // Run the dialog & check that the user didn't cancel - if (dialog_open.ShowModal() == wxID_OK) - { - wxBeginBusyCursor(); - - // Get an array of selected filenames - wxArrayString files; - dialog_open.GetPaths(files); - - // Open them - openFiles(files); - - wxEndBusyCursor(); - - // Save 'dir_last' - dir_last = wxutil::strToView(dialog_open.GetDirectory()); - } + // and filters by wad, zip and pk3 file extensions, then open all files + // that were selected (if any) + auto inf = filedialog::openFiles("Choose file(s) to open", extensions, this); + for (const auto& file : inf.filenames) + openFile(file); } // File->Open Directory else if (id == "aman_opendir") { - // Open a directory browser dialog - wxDirDialog dialog_open( - this, "Select a Directory to open", dir_last, wxDD_DIR_MUST_EXIST | wxDD_NEW_DIR_BUTTON); - - // Run the dialog & check the user didn't cancel - if (dialog_open.ShowModal() == wxID_OK) - { - wxBeginBusyCursor(); - - // Open directory - openDirAsArchive(dialog_open.GetPath()); - - wxEndBusyCursor(); - - // Save 'dir_last' - dir_last = wxutil::strToView(dialog_open.GetPath()); - } + auto dir_path = filedialog::openDirectory("Select a Directory to open", this); + if (!dir_path.empty()) + openDirAsArchive(dir_path); } // File->Recent @@ -1948,7 +1903,7 @@ bool ArchiveManagerPanel::handleAction(string_view id) unsigned index = wx_id_offset_; // Open it - openFile(app::archiveManager().recentFile(index)); + openFile(recent_file_paths_[index]); } // File->Save @@ -2203,7 +2158,7 @@ void ArchiveManagerPanel::onListArchivesRightClick(wxListEvent& e) void ArchiveManagerPanel::onListRecentActivated(wxListEvent& e) { // Open the archive - openFile(app::archiveManager().recentFile(e.GetIndex())); + openFile(recent_file_paths_[e.GetIndex()]); // Refresh the list refreshRecentFileList(); } @@ -2217,7 +2172,7 @@ void ArchiveManagerPanel::onListRecentRightClick(wxListEvent& e) // Generate context menu wxMenu context; SAction::fromId("aman_recent_open")->addToMenu(&context, true); - SAction::fromId("aman_recent_remove")->addToMenu(&context, true); + // SAction::fromId("aman_recent_remove")->addToMenu(&context, true); // Pop it up PopupMenu(&context); @@ -2382,10 +2337,19 @@ void ArchiveManagerPanel::connectSignals() }); // When an archive is opened, open its tab - signal_connections += signals.archive_opened.connect([this](int index) { openTab(index); }); + signal_connections += signals.archive_opened.connect( + [this](int index) + { + ui::setSplashProgressMessage("Initializing UI"); + ui::setSplashProgress(1.0f); + openTab(index); + }); - // Refresh recent files list when changed - signal_connections += signals.recent_files_changed.connect([this]() { refreshRecentFileList(); }); + // Refresh recent files list when library updated + signal_connections += library::signals().archive_file_updated.connect([this](int64_t) { refreshRecentFileList(); }); + signal_connections += library::signals().archive_file_inserted.connect([this](int64_t) + { refreshRecentFileList(); }); + signal_connections += library::signals().archive_file_deleted.connect([this](int64_t) { refreshRecentFileList(); }); // Refresh bookmarks list when changed signal_connections += signals.bookmark_added.connect([this](ArchiveEntry*) { refreshBookmarkList(); }); diff --git a/src/MainEditor/UI/ArchiveManagerPanel.h b/src/MainEditor/UI/ArchiveManagerPanel.h index 3bcd7315c..aa8cc4946 100644 --- a/src/MainEditor/UI/ArchiveManagerPanel.h +++ b/src/MainEditor/UI/ArchiveManagerPanel.h @@ -32,7 +32,7 @@ class ArchiveManagerPanel : public DockPanel, SActionHandler void disableArchiveListUpdate() const; void refreshArchiveList() const; - void refreshRecentFileList() const; + void refreshRecentFileList(); void refreshBookmarkList() const; void refreshAllTabs() const; void updateOpenListItem(int index) const; @@ -84,7 +84,7 @@ class ArchiveManagerPanel : public DockPanel, SActionHandler void createNewArchive(const wxString& format) const; bool closeAll(); - void saveAll() const; + void saveAll(); void checkDirArchives(); // Selected archives in the lists @@ -134,6 +134,7 @@ class ArchiveManagerPanel : public DockPanel, SActionHandler bool asked_save_unchanged_ = false; bool checked_dir_archive_changes_ = false; vector checking_archives_; + wxArrayString recent_file_paths_; // Signal connections ScopedConnectionList signal_connections; diff --git a/src/MainEditor/UI/ArchivePanel.cpp b/src/MainEditor/UI/ArchivePanel.cpp index 99ba0f22b..45552eec4 100644 --- a/src/MainEditor/UI/ArchivePanel.cpp +++ b/src/MainEditor/UI/ArchivePanel.cpp @@ -60,6 +60,7 @@ #include "General/UndoRedo.h" #include "General/UndoSteps/EntryDataUS.h" #include "Graphics/Palette/PaletteManager.h" +#include "Library/ArchiveUIConfig.h" #include "MainEditor/ArchiveOperations.h" #include "MainEditor/Conversions.h" #include "MainEditor/EntryOperations.h" @@ -81,6 +82,7 @@ #include "UI/Lists/ArchiveEntryTree.h" #include "UI/SToolBar/SToolBar.h" #include "UI/SToolBar/SToolBarButton.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/SFileDialog.h" #include "Utility/StringUtils.h" @@ -103,8 +105,6 @@ CVAR(Bool, confirm_entry_delete, true, CVar::Flag::Save) CVAR(Bool, context_submenus, true, CVar::Flag::Save) CVAR(Bool, auto_entry_replace, false, CVar::Flag::Save) CVAR(Bool, elist_show_filter, false, CVar::Flag::Save) -CVAR(Int, ap_splitter_position_tree, 300, CVar::Flag::Save) -CVAR(Int, ap_splitter_position_list, 300, CVar::Flag::Save) CVAR(Bool, elist_no_tree, false, CVar::Flag::Save) @@ -335,14 +335,17 @@ ArchivePanel::ArchivePanel(wxWindow* parent, const shared_ptr& archive) undo_manager_{ new UndoManager() }, ee_manager_{ new ExternalEditManager } { - setup(archive.get()); - bindEvents(archive.get()); + if (archive) + { + setup(*archive); + bindEvents(archive.get()); + } } // ----------------------------------------------------------------------------- // Setup the panel controls and layout // ----------------------------------------------------------------------------- -void ArchivePanel::setup(const Archive* archive) +void ArchivePanel::setup(const Archive& archive) { // Create controls splitter_ = new ui::Splitter(this, -1, wxSP_3DSASH | wxSP_LIVE_UPDATE); @@ -361,10 +364,11 @@ void ArchivePanel::setup(const Archive* archive) // Setup splitter splitter_->SetMinimumPaneSize(ui::scalePx(300)); - m_hbox->Add(splitter_, wxutil::sfWithBorder(1).Expand()); - int split_pos = ap_splitter_position_list; - if (archive && archive->formatInfo().supports_dirs) - split_pos = ap_splitter_position_tree; + m_hbox->Add(splitter_, wxSizerFlags(1).Expand().Border(wxALL, ui::pad())); + auto split_pos = library::archiveUIConfigSplitterPos(archive.libraryId()); + if (split_pos < 0) + split_pos = ui::getStateInt( + archive.formatInfo().supports_dirs ? "ArchivePanelSplitPosTree" : "ArchivePanelSplitPosList"); splitter_->SplitVertically(elist_panel, cur_area_, split_pos); // Update size+layout @@ -395,9 +399,11 @@ void ArchivePanel::bindEvents(Archive* archive) if (auto archive = archive_.lock().get()) { if (archive->formatInfo().supports_dirs) - ap_splitter_position_tree = e.GetSashPosition(); + ui::saveStateInt("ArchivePanelSplitPosTree", e.GetSashPosition()); else - ap_splitter_position_list = e.GetSashPosition(); + ui::saveStateInt("ArchivePanelSplitPosList", e.GetSashPosition()); + + library::saveArchiveUIConfigSplitterPos(archive->libraryId(), e.GetSashPosition()); } }); @@ -2514,21 +2520,7 @@ bool ArchivePanel::handleAction(string_view id) { RunDialog dlg(this, archive.get()); if (dlg.ShowModal() == wxID_OK) - { - wxString command = dlg.selectedCommandLine(archive.get(), ""); - if (!command.IsEmpty()) - { - // Set working directory - wxString wd = wxGetCwd(); - wxSetWorkingDirectory(dlg.selectedExeDir()); - - // Run - wxExecute(command, wxEXEC_ASYNC); - - // Restore working directory - wxSetWorkingDirectory(wd); - } - } + dlg.run(RunDialog::Config{ archive->filename() }, archive->libraryId()); return true; } @@ -3349,7 +3341,10 @@ void ArchivePanel::onEntryListActivated(wxDataViewEvent& e) // Attempt to open map if (mapeditor::window()->openMap(info)) + { + dlg.saveConfigToDatabase(); mapeditor::window()->Show(); + } else { mapeditor::window()->Hide(); diff --git a/src/MainEditor/UI/ArchivePanel.h b/src/MainEditor/UI/ArchivePanel.h index 41017e380..599591c00 100644 --- a/src/MainEditor/UI/ArchivePanel.h +++ b/src/MainEditor/UI/ArchivePanel.h @@ -168,7 +168,7 @@ class ArchivePanel : public wxPanel, SActionHandler void onBtnClearFilter(wxCommandEvent& e); private: - void setup(const Archive* archive); + void setup(const Archive& archive); void bindEvents(Archive* archive); wxPanel* createEntryListPanel(wxWindow* parent); }; diff --git a/src/MainEditor/UI/EntryPanel/GfxEntryPanel.cpp b/src/MainEditor/UI/EntryPanel/GfxEntryPanel.cpp index bbffb3314..40beaddd5 100644 --- a/src/MainEditor/UI/EntryPanel/GfxEntryPanel.cpp +++ b/src/MainEditor/UI/EntryPanel/GfxEntryPanel.cpp @@ -57,6 +57,7 @@ #include "UI/SBrush.h" #include "UI/SToolBar/SToolBar.h" #include "UI/SToolBar/SToolBarButton.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/Colour.h" #include "Utility/StringUtils.h" @@ -67,13 +68,10 @@ using namespace slade; // ----------------------------------------------------------------------------- // -// Variables +// External Variables // // ----------------------------------------------------------------------------- EXTERN_CVAR(Bool, gfx_arc) -EXTERN_CVAR(String, last_colour) -EXTERN_CVAR(String, last_tint_colour) -EXTERN_CVAR(Int, last_tint_amount) // ----------------------------------------------------------------------------- @@ -868,7 +866,7 @@ bool GfxEntryPanel::handleEntryPanelAction(string_view id) { auto* pal = maineditor::currentPalette(); GfxColouriseDialog gcd(theMainWindow, entry.get(), *pal); - gcd.setColour(last_colour); + gcd.setColour(ui::getStateString("ColouriseDialogLastColour")); // Show colourise dialog if (gcd.ShowModal() == wxID_OK) @@ -884,7 +882,7 @@ bool GfxEntryPanel::handleEntryPanelAction(string_view id) Refresh(); setModified(); } - last_colour = colour::toString(gcd.colour(), colour::StringFormat::RGB); + ui::saveStateString("ColouriseDialogLastColour", colour::toString(gcd.colour(), colour::StringFormat::RGB)); } // Tint @@ -892,7 +890,7 @@ bool GfxEntryPanel::handleEntryPanelAction(string_view id) { auto* pal = maineditor::currentPalette(); GfxTintDialog gtd(theMainWindow, entry.get(), *pal); - gtd.setValues(last_tint_colour, last_tint_amount); + gtd.setValues(ui::getStateString("TintDialogLastColour"), ui::getStateInt("TintDialogLastAmount")); // Show tint dialog if (gtd.ShowModal() == wxID_OK) @@ -908,8 +906,8 @@ bool GfxEntryPanel::handleEntryPanelAction(string_view id) Refresh(); setModified(); } - last_tint_colour = colour::toString(gtd.colour(), colour::StringFormat::RGB); - last_tint_amount = static_cast(gtd.amount() * 100.0); + ui::saveStateString("TintDialogLastColour", colour::toString(gtd.colour(), colour::StringFormat::RGB)); + ui::saveStateInt("TintDialogLastAmount", static_cast(gtd.amount() * 100.0f)); } // Crop diff --git a/src/MainEditor/UI/EntryPanel/MapEntryPanel.cpp b/src/MainEditor/UI/EntryPanel/MapEntryPanel.cpp index 319b98b04..00f9a9c75 100644 --- a/src/MainEditor/UI/EntryPanel/MapEntryPanel.cpp +++ b/src/MainEditor/UI/EntryPanel/MapEntryPanel.cpp @@ -59,7 +59,6 @@ CVAR(Int, map_image_height, -5, CVar::Flag::Save) // External Variables // // ----------------------------------------------------------------------------- -EXTERN_CVAR(String, dir_last) EXTERN_CVAR(Bool, map_view_things) diff --git a/src/MainEditor/UI/EntryPanel/TextEntryPanel.cpp b/src/MainEditor/UI/EntryPanel/TextEntryPanel.cpp index 17e7461e2..b0ce96176 100644 --- a/src/MainEditor/UI/EntryPanel/TextEntryPanel.cpp +++ b/src/MainEditor/UI/EntryPanel/TextEntryPanel.cpp @@ -148,10 +148,6 @@ bool TextEntryPanel::loadEntry(ArchiveEntry* entry) if (!text_area_->loadEntry(entry)) return false; - // Scroll to previous position (if any) - if (auto pos = entry->exProps().getIf("TextPosition")) - text_area_->GotoPos(*pos); - // --- Attempt to determine text language --- TextLanguage* tl = nullptr; @@ -189,6 +185,10 @@ bool TextEntryPanel::loadEntry(ArchiveEntry* entry) // Prevent undoing loading the entry text_area_->EmptyUndoBuffer(); + // Scroll to previous position (if any) + if (auto pos = entry->exProps().getIf("TextPosition")) + text_area_->GotoPos(*pos); + // Update variables setModified(false); @@ -250,7 +250,13 @@ void TextEntryPanel::closeEntry() return; // Save current caret position - entry->exProp("TextPosition") = text_area_->GetCurrentPos(); + auto text_pos = text_area_->GetCurrentPos(); + if (text_pos > 0) + entry->exProp("TextPosition") = text_pos; + else + entry->exProps().remove("TextPosition"); + + EntryPanel::closeEntry(); } // ----------------------------------------------------------------------------- diff --git a/src/MainEditor/UI/MainWindow.cpp b/src/MainEditor/UI/MainWindow.cpp index f269aecf2..baf1b12b2 100644 --- a/src/MainEditor/UI/MainWindow.cpp +++ b/src/MainEditor/UI/MainWindow.cpp @@ -37,9 +37,10 @@ #include "Archive/ArchiveManager.h" #include "ArchiveManagerPanel.h" #include "ArchivePanel.h" -#include "General/Misc.h" #include "General/SAction.h" +#include "General/UI.h" #include "Graphics/Icons.h" +#include "Library/UI/LibraryPanel.h" #include "MapEditor/MapEditor.h" #include "SLADEWxApp.h" #include "Scripting/ScriptManager.h" @@ -54,9 +55,9 @@ #include "UI/SAuiTabArt.h" #include "UI/SToolBar/SToolBar.h" #include "UI/SToolBar/SToolBarButton.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/StringUtils.h" -#include "Utility/Tokenizer.h" using namespace slade; @@ -115,7 +116,7 @@ MainWindow::MainWindow() : STopWindow("SLADE", "main") { custom_menus_begin_ = 2; - if (mw_maximized) + if (ui::getStateBool("MainWindowMaximized")) wxTopLevelWindow::Maximize(); setupLayout(); @@ -136,26 +137,11 @@ MainWindow::~MainWindow() // ----------------------------------------------------------------------------- void MainWindow::loadLayout() const { - // Open layout file - Tokenizer tz; - if (!tz.openFile(app::path("mainwindow.layout", app::Dir::User))) - return; - - // Parse layout - while (true) - { - // Read component+layout pair - wxString component = tz.getToken(); - wxString layout = tz.getToken(); + auto layout = ui::getWindowLayout(id_.c_str()); - // Load layout to component - if (!component.IsEmpty() && !layout.IsEmpty()) - aui_mgr_->LoadPaneInfo(layout, aui_mgr_->GetPane(component)); - - // Check if we're done - if (tz.peekToken().empty()) - break; - } + for (const auto& component : layout) + if (!component.first.empty() && !component.second.empty()) + aui_mgr_->LoadPaneInfo(component.second, aui_mgr_->GetPane(component.first)); } // ----------------------------------------------------------------------------- @@ -163,28 +149,13 @@ void MainWindow::loadLayout() const // ----------------------------------------------------------------------------- void MainWindow::saveLayout() const { - // Open layout file - wxFile file(app::path("mainwindow.layout", app::Dir::User), wxFile::write); - - // Write component layout + vector layout; - // Console pane - file.Write("\"console\" "); - wxString pinf = aui_mgr_->SavePaneInfo(aui_mgr_->GetPane("console")); - file.Write(wxString::Format("\"%s\"\n", pinf)); + layout.emplace_back("console", aui_mgr_->SavePaneInfo(aui_mgr_->GetPane("console")).ToStdString()); + layout.emplace_back("archive_manager", aui_mgr_->SavePaneInfo(aui_mgr_->GetPane("archive_manager")).ToStdString()); + layout.emplace_back("undo_history", aui_mgr_->SavePaneInfo(aui_mgr_->GetPane("undo_history")).ToStdString()); - // Archive Manager pane - file.Write("\"archive_manager\" "); - pinf = aui_mgr_->SavePaneInfo(aui_mgr_->GetPane("archive_manager")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Undo History pane - file.Write("\"undo_history\" "); - pinf = aui_mgr_->SavePaneInfo(aui_mgr_->GetPane("undo_history")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Close file - file.Close(); + ui::setWindowLayout(id_.c_str(), layout); } // ----------------------------------------------------------------------------- @@ -305,6 +276,7 @@ void MainWindow::setupLayout() // Tools menu auto tools_menu = new wxMenu(""); + SAction::fromId("main_showlibrary")->addToMenu(tools_menu); SAction::fromId("main_runscript")->addToMenu(tools_menu); menu->Append(tools_menu, "&Tools"); @@ -350,7 +322,14 @@ void MainWindow::setupLayout() b_maint->setMenu(ArchivePanel::createMaintenanceMenu()); toolbar_->addGroup(tbg_archive); - // Create Boomkarks toolbar + // Create General toolbar + auto* tbg_general = new SToolBarGroup(toolbar_, "_General"); + tbg_general->addActionButton("main_showlibrary"); + tbg_general->addActionButton("main_runscript"); + tbg_general->addActionButton("main_preferences"); + toolbar_->addGroup(tbg_general); + + // Create Bookmarks toolbar auto* tbg_bookmarks = new SToolBarGroup(toolbar_, "_Bookmarks"); auto* b_bookmarks = tbg_bookmarks->addActionButton( "bookmarks", "Bookmarks", "bookmark", "Go to a bookmarked entry"); @@ -445,11 +424,15 @@ bool MainWindow::exitProgram() // Save current layout // main_window_layout = aui_mgr_->SavePerspective(); saveLayout(); - mw_maximized = IsMaximized(); + ui::saveStateBool("MainWindowMaximized", IsMaximized()); const wxSize size = GetSize() * GetContentScaleFactor(); if (!IsMaximized()) - misc::setWindowInfo( - id_, size.x, size.y, GetPosition().x * GetContentScaleFactor(), GetPosition().y * GetContentScaleFactor()); + ui::setWindowInfo( + id_.c_str(), + size.x, + size.y, + GetPosition().x * GetContentScaleFactor(), + GetPosition().y * GetContentScaleFactor()); // Save selected palette global_palette = wxutil::strToView(palette_chooser_->GetStringSelection()); @@ -493,6 +476,42 @@ void MainWindow::openStartPageTab() const stc_tabs_->AddPage(new ui::StartPanel(stc_tabs_), "Start Page", true, icons::getIcon(icons::General, "logo")); } +// ----------------------------------------------------------------------------- +// Returns true if the Archive Library tab is currently open +// ----------------------------------------------------------------------------- +bool MainWindow::libraryTabOpen() const +{ + for (unsigned a = 0; a < stc_tabs_->GetPageCount(); a++) + { + if (stc_tabs_->GetPage(a)->GetName() == "library") + return true; + } + + return false; +} + +// ----------------------------------------------------------------------------- +// Switches to the Archive Library tab, or (re)creates it if it has been closed +// ----------------------------------------------------------------------------- +void MainWindow::openLibraryTab() const +{ + // Find existing tab + for (unsigned a = 0; a < stc_tabs_->GetPageCount(); a++) + { + if (stc_tabs_->GetPage(a)->GetName() == "library") + { + stc_tabs_->SetSelection(a); + return; + } + } + + // Not found, create library tab + auto lib_tab = new ui::LibraryPanel(stc_tabs_); + lib_tab->SetName("library"); + stc_tabs_->AddPage(lib_tab, "Archive Library", true); + stc_tabs_->SetPageBitmap(stc_tabs_->GetPageIndex(lib_tab), icons::getIcon(icons::General, "library")); +} + // ----------------------------------------------------------------------------- // Handles the action [id]. // Returns true if the action was handled, false otherwise @@ -573,7 +592,10 @@ bool MainWindow::handleAction(string_view id) // View->Show Start Page if (id == "main_showstartpage") + { openStartPageTab(); + return true; + } #ifndef NO_LUA // Tools->Run Script @@ -584,6 +606,13 @@ bool MainWindow::handleAction(string_view id) } #endif + // Tools->Archive Library + if (id == "main_showlibrary") + { + openLibraryTab(); + return true; + } + // Help->About if (id == "main_about") { @@ -690,7 +719,7 @@ void MainWindow::onSize(wxSizeEvent& e) #endif // Update maximized cvar - mw_maximized = IsMaximized(); + ui::saveStateBool("MainWindowMaximized", IsMaximized()); // Test creation of OpenGL context if (!opengl_test_done && e.GetSize().x > 20 && e.GetSize().y > 20) diff --git a/src/MainEditor/UI/MainWindow.h b/src/MainEditor/UI/MainWindow.h index 75cea2acb..c57b2faa1 100644 --- a/src/MainEditor/UI/MainWindow.h +++ b/src/MainEditor/UI/MainWindow.h @@ -29,6 +29,9 @@ class MainWindow : public STopWindow, SActionHandler bool startPageTabOpen() const; void openStartPageTab() const; + bool libraryTabOpen() const; + void openLibraryTab() const; + ArchiveManagerPanel* archiveManagerPanel() const { return panel_archivemanager_; } PaletteChooser* paletteChooser() const { return palette_chooser_; } UndoManagerHistoryPanel* undoHistoryPanel() const { return panel_undo_history_; } diff --git a/src/MainEditor/UI/StartPanel.cpp b/src/MainEditor/UI/StartPanel.cpp index 8d382c88f..f826a84b7 100644 --- a/src/MainEditor/UI/StartPanel.cpp +++ b/src/MainEditor/UI/StartPanel.cpp @@ -39,6 +39,8 @@ #include "Archive/ArchiveFormat.h" #include "Archive/ArchiveManager.h" #include "General/SActionHandler.h" +#include "General/UI.h" +#include "Library/Library.h" #include "UI/SToolBar/SToolBarButton.h" #include "UI/WxUtils.h" #include "Utility/StringUtils.h" @@ -144,6 +146,7 @@ wxSizer* createActionsSizer(wxWindow* parent) sizer->Add(createActionButton(parent, "aman_opendir", "Open Directory", "opendir"), sflags); sizer->Add(createActionButton(parent, "aman_newarchive", "Create New Archive", "newarchive"), sflags); sizer->Add(createActionButton(parent, "aman_newmap", "Create New Map", "mapeditor"), sflags); + sizer->Add(createActionButton(parent, "main_showlibrary", "View Archive Library", "library"), sflags); return sizer; } @@ -168,8 +171,8 @@ StartPanel::StartPanel(wxWindow* parent) : wxPanel(parent, -1) // Setup Recent Files panel recent_files_panel_ = new wxPanel(this); - sc_recent_files_updated_ = app::archiveManager().signals().recent_files_changed.connect_scoped( - [this] { updateRecentFilesPanel(); }); // Update panel when recent files list changes + sc_recent_files_updated_ = library::signals().archive_file_updated.connect_scoped( + [this](int64_t) { updateRecentFilesPanel(); }); // Update panel when recent files list changes recent_files_panel_->SetBackgroundColour(wxColour(background_colour)); recent_files_panel_->SetForegroundColour(wxColour(foreground_colour)); updateRecentFilesPanel(); @@ -224,7 +227,7 @@ void StartPanel::updateRecentFilesPanel() title_label->SetFont(title_label->GetFont().Bold().Scale(1.25f)); sizer->Add(title_label, wxutil::sfWithBorder(0, wxBOTTOM).Expand()); - auto recent_files = app::archiveManager().recentFiles(); + auto recent_files = library::recentFiles(12); if (recent_files.empty()) { auto no_recent_label = new wxStaticText(recent_files_panel_, -1, "No recently opened files"); diff --git a/src/MainEditor/UI/TextureXEditor/PatchTablePanel.cpp b/src/MainEditor/UI/TextureXEditor/PatchTablePanel.cpp index df7e1cbb1..578befb5e 100644 --- a/src/MainEditor/UI/TextureXEditor/PatchTablePanel.cpp +++ b/src/MainEditor/UI/TextureXEditor/PatchTablePanel.cpp @@ -51,18 +51,11 @@ #include "UI/Lists/VirtualListView.h" #include "UI/SToolBar/SToolBar.h" #include "UI/WxUtils.h" +#include "Utility/SFileDialog.h" using namespace slade; -// ----------------------------------------------------------------------------- -// -// External Variables -// -// ----------------------------------------------------------------------------- -EXTERN_CVAR(String, dir_last) - - // ----------------------------------------------------------------------------- // // PatchTableListView Class @@ -394,7 +387,7 @@ void PatchTablePanel::addPatchFromFile() auto etypes = EntryType::allTypes(); // Go through types - wxString ext_filter = "All files (*.*)|*.*|"; + string ext_filter = "All files (*.*)|*.*|"; for (auto& etype : etypes) { // If the type is a valid image type, add its extension filter @@ -405,32 +398,16 @@ void PatchTablePanel::addPatchFromFile() } } - // Create open file dialog - wxFileDialog dialog_open( - this, - "Choose file(s) to open", - dir_last, - wxEmptyString, - ext_filter, - wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST, - wxDefaultPosition); - - // Run the dialog & check that the user didn't cancel - if (dialog_open.ShowModal() == wxID_OK) + // Show file dialog to open multiple files + filedialog::FDInfo inf; + if (filedialog::openFiles(inf, "Choose file(s) to open", ext_filter, this)) { - // Get file selection - wxArrayString files; - dialog_open.GetPaths(files); - - // Save 'dir_last' - dir_last = wxutil::strToView(dialog_open.GetDirectory()); - // Go through file selection - for (const auto& file : files) + for (const auto& file : inf.filenames) { // Load the file into a temporary ArchiveEntry auto entry = std::make_shared(); - entry->importFile(file.ToStdString()); + entry->importFile(file); // Determine type EntryType::detectEntryType(*entry); diff --git a/src/MainEditor/UI/TextureXEditor/TextureXPanel.cpp b/src/MainEditor/UI/TextureXEditor/TextureXPanel.cpp index 9eb2ee426..fbf058bad 100644 --- a/src/MainEditor/UI/TextureXEditor/TextureXPanel.cpp +++ b/src/MainEditor/UI/TextureXEditor/TextureXPanel.cpp @@ -68,7 +68,6 @@ using namespace slade; // External Variables // // ----------------------------------------------------------------------------- -EXTERN_CVAR(String, dir_last) EXTERN_CVAR(Bool, wad_force_uppercase) @@ -984,7 +983,7 @@ void TextureXPanel::newTextureFromFile() auto etypes = EntryType::allTypes(); // Go through types - wxString ext_filter = "All files (*.*)|*.*|"; + string ext_filter = "All files (*.*)|*.*|"; for (auto& etype : etypes) { // If the type is a valid image type, add its extension filter @@ -995,32 +994,16 @@ void TextureXPanel::newTextureFromFile() } } - // Create open file dialog - wxFileDialog dialog_open( - this, - "Choose file(s) to open", - dir_last, - wxEmptyString, - ext_filter, - wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST, - wxDefaultPosition); - - // Run the dialog & check that the user didn't cancel - if (dialog_open.ShowModal() == wxID_OK) + // Show file dialog to open multiple files + filedialog::FDInfo inf; + if (filedialog::openFiles(inf, "Choose file(s) to open", ext_filter, this)) { - // Get file selection - wxArrayString files; - dialog_open.GetPaths(files); - - // Save 'dir_last' - dir_last = wxutil::strToView(dialog_open.GetDirectory()); - // Go through file selection - for (const auto& file : files) + for (const auto& file : inf.filenames) { // Load the file into a temporary ArchiveEntry auto entry = std::make_shared(); - entry->importFile(file.ToStdString()); + entry->importFile(file); // Determine type EntryType::detectEntryType(*entry); diff --git a/src/MapEditor/MapBackupManager.cpp b/src/MapEditor/MapBackupManager.cpp index 9097c8f22..d30b795ad 100644 --- a/src/MapEditor/MapBackupManager.cpp +++ b/src/MapEditor/MapBackupManager.cpp @@ -86,7 +86,7 @@ bool MapBackupManager::writeBackup( string fname{ archive_name }; std::replace(fname.begin(), fname.end(), '.', '_'); auto backup_file = fmt::format("{}/{}_backup.zip", backup_dir, fname); - if (!backup->open(backup_file)) + if (!backup->open(backup_file, true)) backup->setFilename(backup_file); // Filter ignored entries diff --git a/src/MapEditor/UI/MapBackupPanel.cpp b/src/MapEditor/UI/MapBackupPanel.cpp index d6462fddc..d5a1a64c3 100644 --- a/src/MapEditor/UI/MapBackupPanel.cpp +++ b/src/MapEditor/UI/MapBackupPanel.cpp @@ -84,7 +84,7 @@ bool MapBackupPanel::loadBackups(wxString archive_name, const wxString& map_name // Open backup file archive_name.Replace(".", "_"); auto backup_file = app::path("backups", app::Dir::User) + "/" + archive_name.ToStdString() + "_backup.zip"; - if (!archive_backups_->open(backup_file)) + if (!archive_backups_->open(backup_file, true)) return false; // Get backup dir for map diff --git a/src/MapEditor/UI/MapEditorWindow.cpp b/src/MapEditor/UI/MapEditorWindow.cpp index 373c34df2..277926480 100644 --- a/src/MapEditor/UI/MapEditorWindow.cpp +++ b/src/MapEditor/UI/MapEditorWindow.cpp @@ -38,7 +38,6 @@ #include "Archive/ArchiveManager.h" #include "Game/Configuration.h" #include "Game/Game.h" -#include "General/Misc.h" #include "General/SAction.h" #include "General/UI.h" #include "MainEditor/MainEditor.h" @@ -65,9 +64,9 @@ #include "UI/Dialogs/RunDialog.h" #include "UI/SAuiTabArt.h" #include "UI/SToolBar/SToolBar.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/SFileDialog.h" -#include "Utility/Tokenizer.h" using namespace slade; using namespace mapeditor; @@ -108,7 +107,7 @@ EXTERN_CVAR(Int, flat_drawtype); // ----------------------------------------------------------------------------- MapEditorWindow::MapEditorWindow() : STopWindow{ "SLADE", "map" } { - if (mew_maximized) + if (ui::getStateBool("MapEditorWindowMaximized")) wxTopLevelWindow::Maximize(); setupLayout(); wxTopLevelWindow::Show(false); @@ -138,27 +137,12 @@ MapEditorWindow::~MapEditorWindow() // ----------------------------------------------------------------------------- void MapEditorWindow::loadLayout() { - // Open layout file - Tokenizer tz; - if (!tz.openFile(app::path("mapwindow.layout", app::Dir::User))) - return; - - // Parse layout - auto m_mgr = wxAuiManager::GetManager(this); - while (true) - { - // Read component+layout pair - wxString component = tz.getToken(); - wxString layout = tz.getToken(); + auto* aui_mgr = wxAuiManager::GetManager(this); + auto layout = ui::getWindowLayout(id_.c_str()); - // Load layout to component - if (!component.IsEmpty() && !layout.IsEmpty()) - m_mgr->LoadPaneInfo(layout, m_mgr->GetPane(component)); - - // Check if we're done - if (tz.peekToken().empty()) - break; - } + for (const auto& component : layout) + if (!component.first.empty() && !component.second.empty()) + aui_mgr->LoadPaneInfo(component.second, aui_mgr->GetPane(component.first)); } // ----------------------------------------------------------------------------- @@ -166,39 +150,16 @@ void MapEditorWindow::loadLayout() // ----------------------------------------------------------------------------- void MapEditorWindow::saveLayout() { - // Open layout file - wxFile file(app::path("mapwindow.layout", app::Dir::User), wxFile::write); - - // Write component layout - auto m_mgr = wxAuiManager::GetManager(this); - - // Console pane - file.Write("\"console\" "); - wxString pinf = m_mgr->SavePaneInfo(m_mgr->GetPane("console")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Item info pane - file.Write("\"item_props\" "); - pinf = m_mgr->SavePaneInfo(m_mgr->GetPane("item_props")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Script editor pane - file.Write("\"script_editor\" "); - pinf = m_mgr->SavePaneInfo(m_mgr->GetPane("script_editor")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Map checks pane - file.Write("\"map_checks\" "); - pinf = m_mgr->SavePaneInfo(m_mgr->GetPane("map_checks")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Undo history pane - file.Write("\"undo_history\" "); - pinf = m_mgr->SavePaneInfo(m_mgr->GetPane("undo_history")); - file.Write(wxString::Format("\"%s\"\n", pinf)); - - // Close file - file.Close(); + vector layout; + auto* aui_mgr = wxAuiManager::GetManager(this); + + layout.emplace_back("console", aui_mgr->SavePaneInfo(aui_mgr->GetPane("console")).ToStdString()); + layout.emplace_back("item_props", aui_mgr->SavePaneInfo(aui_mgr->GetPane("item_props")).ToStdString()); + layout.emplace_back("script_editor", aui_mgr->SavePaneInfo(aui_mgr->GetPane("script_editor")).ToStdString()); + layout.emplace_back("map_checks", aui_mgr->SavePaneInfo(aui_mgr->GetPane("map_checks")).ToStdString()); + layout.emplace_back("undo_history", aui_mgr->SavePaneInfo(aui_mgr->GetPane("undo_history")).ToStdString()); + + ui::setWindowLayout(id_.c_str(), layout); } // ----------------------------------------------------------------------------- @@ -594,9 +555,12 @@ bool MapEditorWindow::chooseMap(Archive* archive) wxICON_ERROR); return false; } - else - return true; + + dlg.saveConfigToDatabase(); + + return true; } + return false; } @@ -640,7 +604,7 @@ bool MapEditorWindow::openMap(const MapDesc& map) if (map.archive) { Archive temp(ArchiveFormat::Wad); - temp.open(head->data()); + temp.open(head->data(), true); for (unsigned a = 0; a < temp.numEntries(); a++) map_data_.emplace_back(new ArchiveEntry(*(temp.entryAt(a)))); } @@ -728,7 +692,7 @@ void MapEditorWindow::loadMapScripts(const MapDesc& map) if (map.archive) { auto wad = std::make_unique(ArchiveFormat::Wad); - wad->open(head->data()); + wad->open(head->data(), true); auto maps = wad->detectMaps(); if (!maps.empty()) { @@ -825,7 +789,7 @@ void MapEditorWindow::buildNodes(Archive* wad) // Re-load wad wad->close(); - wad->open(filename); + wad->open(filename, true); } else if (nb_warned) log::info(1, "Nodebuilder path not set up, no nodes were built"); @@ -916,7 +880,7 @@ bool MapEditorWindow::saveMap() if (mdesc_current.archive && current_head) { tempwad = std::make_unique(ArchiveFormat::Wad); - tempwad->open(current_head.get()); + tempwad->open(current_head.get(), true); auto amaps = tempwad->detectMaps(); if (!amaps.empty()) map = amaps[0]; @@ -1000,7 +964,6 @@ bool MapEditorWindow::saveMapAs() // Write wad to file wad.save(info.filenames[0]); auto archive = app::archiveManager().openArchive(info.filenames[0], true, true); - app::archiveManager().addRecentFile(info.filenames[0]); // Update current map description auto maps = archive->detectMaps(); @@ -1390,19 +1353,10 @@ bool MapEditorWindow::handleAction(string_view id) if (dlg.start3dModeChecked() || id == "mapw_run_map_here") mapeditor::editContext().resetPlayerStart(); - wxString command = dlg.selectedCommandLine(archive, mdesc_current.name, wad.filename()); - if (!command.IsEmpty()) - { - // Set working directory - wxString wd = wxGetCwd(); - wxSetWorkingDirectory(dlg.selectedExeDir()); - - // Run - wxExecute(command, wxEXEC_ASYNC); - - // Restore working directory - wxSetWorkingDirectory(wd); - } + RunDialog::Config cfg{ archive->filename() }; + cfg.map_name = mdesc_current.name; + cfg.map_file = wad.filename(); + dlg.run(cfg, archive->libraryId()); } return true; @@ -1457,8 +1411,12 @@ void MapEditorWindow::onClose(wxCloseEvent& e) saveLayout(); const wxSize size = GetSize() * GetContentScaleFactor(); if (!IsMaximized()) - misc::setWindowInfo( - id_, size.x, size.y, GetPosition().x * GetContentScaleFactor(), GetPosition().y * GetContentScaleFactor()); + ui::setWindowInfo( + id_.c_str(), + size.x, + size.y, + GetPosition().x * GetContentScaleFactor(), + GetPosition().y * GetContentScaleFactor()); Show(false); closeMap(); @@ -1470,7 +1428,7 @@ void MapEditorWindow::onClose(wxCloseEvent& e) void MapEditorWindow::onSize(wxSizeEvent& e) { // Update maximized cvar - mew_maximized = IsMaximized(); + ui::saveStateBool("MapEditorWindowMaximized", IsMaximized()); e.Skip(); } diff --git a/src/OpenGL/Draw2D.cpp b/src/OpenGL/Draw2D.cpp index 92b6c025a..c9ff18bf5 100644 --- a/src/OpenGL/Draw2D.cpp +++ b/src/OpenGL/Draw2D.cpp @@ -1,7 +1,7 @@ // ----------------------------------------------------------------------------- // SLADE - It's a Doom Editor -// Copyright(C) 2008 - 2022 Simon Judd +// Copyright(C) 2008 - 2024 Simon Judd // // Email: sirjuddington@gmail.com // Web: http://slade.mancubus.net diff --git a/src/SLADEMap/SLADEMap.cpp b/src/SLADEMap/SLADEMap.cpp index 283e00b0c..f3e55c0f2 100644 --- a/src/SLADEMap/SLADEMap.cpp +++ b/src/SLADEMap/SLADEMap.cpp @@ -212,7 +212,7 @@ bool SLADEMap::readMap(const MapDesc& map) auto m_head = map.head.lock(); if (map.archive && m_head) { - tempwad.open(m_head->data()); + tempwad.open(m_head->data(), true); auto amaps = tempwad.detectMaps(); if (!amaps.empty()) omap = amaps[0]; diff --git a/src/Scripting/Export/Archives.cpp b/src/Scripting/Export/Archives.cpp index 024096615..befab4407 100644 --- a/src/Scripting/Export/Archives.cpp +++ b/src/Scripting/Export/Archives.cpp @@ -39,6 +39,7 @@ #include "Archive/ArchiveManager.h" #include "Archive/EntryType/EntryType.h" #include "Export.h" +#include "Library/Library.h" #include "thirdparty/sol/sol.hpp" using namespace slade; @@ -138,7 +139,7 @@ void registerArchivesNamespace(sol::state& lua) archives.set_function("BaseResourcePaths", []() { return app::archiveManager().baseResourcePaths(); }); archives.set_function("OpenBaseResource", [](int index) { return app::archiveManager().openBaseResource(index); }); archives.set_function("ProgramResource", []() { return app::archiveManager().programResourceArchive(); }); - archives.set_function("RecentFiles", []() { return app::archiveManager().recentFiles(); }); + archives.set_function("RecentFiles", []() { return library::recentFiles(); }); archives.set_function("Bookmarks", []() { return app::archiveManager().bookmarks(); }); archives.set_function( "AddBookmark", [](ArchiveEntry* entry) { app::archiveManager().addBookmark(entry->getShared()); }); diff --git a/src/Scripting/UI/ScriptManagerWindow.cpp b/src/Scripting/UI/ScriptManagerWindow.cpp index aba5d3da2..51a2104d7 100644 --- a/src/Scripting/UI/ScriptManagerWindow.cpp +++ b/src/Scripting/UI/ScriptManagerWindow.cpp @@ -35,7 +35,6 @@ #include "Archive/Archive.h" #include "Archive/ArchiveEntry.h" #include "Archive/ArchiveManager.h" -#include "General/Misc.h" #include "General/SAction.h" #include "General/UI.h" #include "Graphics/Icons.h" @@ -48,6 +47,7 @@ #include "UI/Controls/STabCtrl.h" #include "UI/SAuiTabArt.h" #include "UI/SToolBar/SToolBar.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/StringUtils.h" @@ -62,8 +62,7 @@ using namespace scriptmanager; // ----------------------------------------------------------------------------- namespace { -wxString docs_url = "https://slade.readthedocs.io/en/latest"; -int layout_version = 1; +wxString docs_url = "https://slade.readthedocs.io/en/latest"; } // namespace CVAR(Bool, sm_maximized, false, CVar::Flag::Save) @@ -227,33 +226,12 @@ ScriptManagerWindow::ScriptManagerWindow() : // ----------------------------------------------------------------------------- void ScriptManagerWindow::loadLayout() { - // Open layout file - wxFile file(app::path("scriptmanager.layout", app::Dir::User), wxFile::read); + auto* aui_mgr = wxAuiManager::GetManager(this); + auto layout = ui::getWindowLayout(id_.c_str()); - // Read component layout - if (file.IsOpened()) - { - wxString text, layout; - file.ReadAll(&text); - - // Get layout version - wxString version = text.BeforeFirst('\n', &layout); - - // Check version - long val; - if (version.ToLong(&val)) - { - // Load layout only if correct version - if (val == layout_version) - wxAuiManager::GetManager(this)->LoadPerspective(layout); - } - } - - // Close file - file.Close(); - - // Force calculated toolbar size - wxAuiManager::GetManager(this)->GetPane("toolbar").MinSize(-1, SToolBar::getBarHeight()); + for (const auto& component : layout) + if (!component.first.empty() && !component.second.empty()) + aui_mgr->LoadPaneInfo(component.second, aui_mgr->GetPane(component.first)); } // ----------------------------------------------------------------------------- @@ -261,15 +239,13 @@ void ScriptManagerWindow::loadLayout() // ----------------------------------------------------------------------------- void ScriptManagerWindow::saveLayout() { - // Open layout file - wxFile file(app::path("scriptmanager.layout", app::Dir::User), wxFile::write); + vector layout; + auto* aui_mgr = wxAuiManager::GetManager(this); - // Write component layout - file.Write(wxString::Format("%d\n", layout_version)); - file.Write(wxAuiManager::GetManager(this)->SavePerspective()); + layout.emplace_back("console", aui_mgr->SavePaneInfo(aui_mgr->GetPane("console")).ToStdString()); + layout.emplace_back("scripts_area", aui_mgr->SavePaneInfo(aui_mgr->GetPane("scripts_area")).ToStdString()); - // Close file - file.Close(); + ui::setWindowLayout(id_.c_str(), layout); } // ----------------------------------------------------------------------------- @@ -278,7 +254,7 @@ void ScriptManagerWindow::saveLayout() void ScriptManagerWindow::setupLayout() { // Maximize if it was last time - if (sm_maximized) + if (ui::getStateBool("ScriptManagerWindowMaximized")) Maximize(); // Create the wxAUI manager & related things @@ -313,13 +289,14 @@ void ScriptManagerWindow::setupLayout() // Setup panel info & add panel p_inf.DefaultPane(); - p_inf.Float(); + p_inf.Bottom(); p_inf.FloatingSize(wxutil::scaledSize(600, 400)); p_inf.FloatingPosition(100, 100); p_inf.MinSize(wxutil::scaledSize(-1, 192)); p_inf.Show(false); p_inf.Caption("Console"); p_inf.Name("console"); + p_inf.Dock(); m_mgr->AddPane(panel_console, p_inf); // Setup menu and toolbar @@ -471,11 +448,11 @@ void ScriptManagerWindow::bindEvents() { // Save Layout saveLayout(); - sm_maximized = IsMaximized(); + ui::saveStateBool("ScriptManagerWindowMaximized", IsMaximized()); const wxSize size = GetSize() * GetContentScaleFactor(); if (!IsMaximized()) - misc::setWindowInfo( - id_, + ui::setWindowInfo( + id_.c_str(), size.x, size.y, GetPosition().x * GetContentScaleFactor(), diff --git a/src/UI/Browser/BrowserWindow.cpp b/src/UI/Browser/BrowserWindow.cpp index 72d113701..659fec2dc 100644 --- a/src/UI/Browser/BrowserWindow.cpp +++ b/src/UI/Browser/BrowserWindow.cpp @@ -37,8 +37,9 @@ #include "BrowserCanvas.h" #include "BrowserItem.h" #include "BrowserWindow.h" -#include "General/Misc.h" +#include "General/UI.h" #include "Graphics/Palette/Palette.h" +#include "UI/State.h" #include "UI/WxUtils.h" using namespace slade; @@ -200,14 +201,14 @@ BrowserWindow::BrowserWindow(wxWindow* parent, bool truncate_names) : namespace wx = wxutil; // Init size/pos - auto info = misc::getWindowInfo("browser"); + auto info = ui::getWindowInfo("browser"); if (!info.id.empty()) { SetClientSize(info.width, info.height); SetPosition(wxPoint(info.left, info.top)); } else - misc::setWindowInfo("browser", 768, 600, 0, 0); + ui::setWindowInfo("browser", 768, 600, 0, 0); // Init variables items_root_ = new BrowserTreeNode(); @@ -287,7 +288,7 @@ BrowserWindow::BrowserWindow(wxWindow* parent, bool truncate_names) : wxWindowBase::Layout(); wxTopLevelWindowBase::SetMinSize(wxutil::scaledSize(540, 400)); - if (browser_maximised) + if (ui::getStateBool("BrowserWindowMaximized")) wxTopLevelWindow::Maximize(); else CenterOnParent(); @@ -301,11 +302,15 @@ BrowserWindow::BrowserWindow(wxWindow* parent, bool truncate_names) : // ----------------------------------------------------------------------------- BrowserWindow::~BrowserWindow() { - auto scale = wxWindowBase::GetContentScaleFactor(); - browser_maximised = wxTopLevelWindow::IsMaximized(); - const wxSize ClientSize = GetClientSize() * scale; + ui::saveStateBool("BrowserWindowMaximized", wxTopLevelWindow::IsMaximized()); + const wxSize client_size = GetClientSize() * GetContentScaleFactor(); if (!wxTopLevelWindow::IsMaximized()) - misc::setWindowInfo("browser", ClientSize.x, ClientSize.y, GetPosition().x * scale, GetPosition().y * scale); + ui::setWindowInfo( + "browser", + client_size.x, + client_size.y, + GetPosition().x * GetContentScaleFactor(), + GetPosition().y * GetContentScaleFactor()); } // ----------------------------------------------------------------------------- diff --git a/src/UI/Controls/ResourceArchiveChooser.cpp b/src/UI/Controls/ResourceArchiveChooser.cpp index 95a41cd8c..5acd1e54b 100644 --- a/src/UI/Controls/ResourceArchiveChooser.cpp +++ b/src/UI/Controls/ResourceArchiveChooser.cpp @@ -36,6 +36,7 @@ #include "Archive/Archive.h" #include "Archive/ArchiveManager.h" #include "General/UI.h" +#include "Library/Library.h" #include "UI/WxUtils.h" #include "Utility/SFileDialog.h" @@ -156,16 +157,14 @@ void ResourceArchiveChooser::onBtnOpenResource(wxCommandEvent& e) // ----------------------------------------------------------------------------- void ResourceArchiveChooser::onBtnRecent(wxCommandEvent& e) { - // Build list of recent wad filename strings - wxArrayString recent; - for (unsigned a = 0; a < app::archiveManager().numRecentFiles(); a++) - recent.Add(app::archiveManager().recentFile(a)); + // Get list of recent filename strings + auto recent = wxutil::arrayStringStd(library::recentFiles()); // Show dialog wxSingleChoiceDialog dlg(this, "Select a recent Archive to open", "Open Recent", recent); if (dlg.ShowModal() == wxID_OK) { - auto na = app::archiveManager().openArchive(app::archiveManager().recentFile(dlg.GetSelection()), true, true); + auto na = app::archiveManager().openArchive(wxutil::strToView(recent[dlg.GetSelection()]), true, true); if (na) { list_resources_->Append(na->filename(false)); diff --git a/src/UI/Controls/ZoomControl.cpp b/src/UI/Controls/ZoomControl.cpp index d93e23077..dde41d510 100644 --- a/src/UI/Controls/ZoomControl.cpp +++ b/src/UI/Controls/ZoomControl.cpp @@ -37,6 +37,8 @@ #include "UI/Canvas/CTextureCanvasBase.h" #include "UI/Canvas/GfxCanvasBase.h" #include "UI/SToolBar/SToolBarButton.h" +#include "UI/State.h" +#include using namespace slade; using namespace ui; @@ -47,8 +49,6 @@ using namespace ui; // Variables // // ----------------------------------------------------------------------------- -CVAR(Int, zoom_gfx, 100, CVar::Save) -CVAR(Int, zoom_ctex, 100, CVar::Save) namespace slade::ui { std::array zoom_percents = { 25, 50, 75, 100, 150, 200, 400, 800 }; @@ -76,9 +76,9 @@ ZoomControl::ZoomControl(wxWindow* parent) : wxPanel(parent, -1) // ----------------------------------------------------------------------------- ZoomControl::ZoomControl(wxWindow* parent, GfxCanvasBase* linked_canvas) : wxPanel(parent, -1), - linked_gfx_canvas_{ linked_canvas }, - zoom_(zoom_gfx) + linked_gfx_canvas_{ linked_canvas } { + zoom_ = getStateInt("ZoomGfxCanvas"); linked_canvas->linkZoomControl(this); linked_canvas->setScale(zoomScale()); setup(); @@ -89,9 +89,9 @@ ZoomControl::ZoomControl(wxWindow* parent, GfxCanvasBase* linked_canvas) : // ----------------------------------------------------------------------------- ZoomControl::ZoomControl(wxWindow* parent, CTextureCanvasBase* linked_canvas) : wxPanel(parent, -1), - linked_texture_canvas_{ linked_canvas }, - zoom_(zoom_ctex) + linked_texture_canvas_{ linked_canvas } { + zoom_ = getStateInt("ZoomCTextureCanvas"); linked_canvas->linkZoomControl(this); linked_canvas->setScale(zoomScale()); setup(); @@ -111,13 +111,13 @@ void ZoomControl::setZoomPercent(int percent) { linked_gfx_canvas_->setScale(zoomScale()); linked_gfx_canvas_->window()->Refresh(); - zoom_gfx = zoom_; + saveStateInt("ZoomGfxCanvas", zoom_); } if (linked_texture_canvas_) { linked_texture_canvas_->setScale(zoomScale()); linked_texture_canvas_->redraw(false); - zoom_ctex = zoom_; + saveStateInt("ZoomCTextureCanvas", zoom_); } } diff --git a/src/UI/Dialogs/MapEditorConfigDialog.cpp b/src/UI/Dialogs/MapEditorConfigDialog.cpp index 6cd10393d..dea22ebfa 100644 --- a/src/UI/Dialogs/MapEditorConfigDialog.cpp +++ b/src/UI/Dialogs/MapEditorConfigDialog.cpp @@ -42,6 +42,7 @@ #include "General/MapPreviewData.h" #include "General/UI.h" #include "Graphics/Icons.h" +#include "Library/ArchiveMapConfig.h" #include "UI/Canvas/Canvas.h" #include "UI/Controls/BaseResourceChooser.h" #include "UI/Controls/ResourceArchiveChooser.h" @@ -201,6 +202,17 @@ MapEditorConfigDialog::MapEditorConfigDialog(wxWindow* parent, Archive* archive, { namespace wx = wxutil; + // Check for saved game/port configuration for archive + if (archive) + { + if (auto archive_map_config = library::getArchiveMapConfig(archive->libraryId()); + archive_map_config.archive_id >= 0) + { + game_current_ = archive_map_config.game; + port_current_ = archive_map_config.port; + } + } + // Setup main sizer auto mainsizer = new wxBoxSizer(wxHORIZONTAL); SetSizer(mainsizer); @@ -525,6 +537,21 @@ wxString MapEditorConfigDialog::selectedPort() return ports_list_[choice_port_config_->GetSelection() - 1]; } +// ----------------------------------------------------------------------------- +// Saves the current selected configuration to the SLADE program database +// ----------------------------------------------------------------------------- +void MapEditorConfigDialog::saveConfigToDatabase() const +{ + if (!archive_) + return; + + library::ArchiveMapConfigRow row{ archive_->libraryId() }; + row.game = game_current_; + row.port = port_current_; + + library::saveArchiveMapConfig(row); +} + // ----------------------------------------------------------------------------- // diff --git a/src/UI/Dialogs/MapEditorConfigDialog.h b/src/UI/Dialogs/MapEditorConfigDialog.h index 0b0034c11..3d79ea2ef 100644 --- a/src/UI/Dialogs/MapEditorConfigDialog.h +++ b/src/UI/Dialogs/MapEditorConfigDialog.h @@ -29,6 +29,7 @@ class MapEditorConfigDialog : public SDialog bool configMatchesMap(const MapDesc& map) const; wxString selectedGame(); wxString selectedPort(); + void saveConfigToDatabase() const; private: wxChoice* choice_game_config_ = nullptr; diff --git a/src/UI/Dialogs/NewArchiveDialog.cpp b/src/UI/Dialogs/NewArchiveDialog.cpp index 63ab88150..0a6b4d81c 100644 --- a/src/UI/Dialogs/NewArchiveDialog.cpp +++ b/src/UI/Dialogs/NewArchiveDialog.cpp @@ -36,20 +36,13 @@ #include "Archive/ArchiveManager.h" #include "General/UI.h" #include "NewArchiveDiaog.h" +#include "UI/State.h" #include "UI/WxUtils.h" using namespace slade; using namespace ui; -// ----------------------------------------------------------------------------- -// -// Variables -// -// ----------------------------------------------------------------------------- -CVAR(String, archive_last_created_format, "wad", CVar::Save) - - // ----------------------------------------------------------------------------- // // NewArchiveDialog Class Functions @@ -72,10 +65,11 @@ NewArchiveDialog::NewArchiveDialog(wxWindow* parent) : wxDialog(parent, -1, "Cre // Fill formats list long selected_index = 0; + auto last_format = ui::getStateString("ArchiveLastCreatedFormat"); for (const auto& format : archive::allFormatsInfo()) if (format.create) { - if (format.id == archive_last_created_format) + if (format.id == last_format) selected_index = choice_type->GetCount(); choice_type->AppendString(format.name + " Archive"); @@ -100,8 +94,8 @@ NewArchiveDialog::NewArchiveDialog(wxWindow* parent) : wxDialog(parent, -1, "Cre for (const auto& format : archive::allFormatsInfo()) if (choice_type->GetString(choice_type->GetSelection()) == (format.name + " Archive")) { - archive_created_ = app::archiveManager().newArchive(format.id).get(); - archive_last_created_format = format.id; + archive_created_ = app::archiveManager().newArchive(format.id).get(); + ui::saveStateString("ArchiveLastCreatedFormat", format.id); EndModal(wxID_OK); } }); diff --git a/src/UI/Dialogs/Preferences/AudioPrefsPanel.cpp b/src/UI/Dialogs/Preferences/AudioPrefsPanel.cpp index e3681a78e..5f9daabe6 100644 --- a/src/UI/Dialogs/Preferences/AudioPrefsPanel.cpp +++ b/src/UI/Dialogs/Preferences/AudioPrefsPanel.cpp @@ -49,7 +49,6 @@ EXTERN_CVAR(Bool, snd_autoplay) EXTERN_CVAR(Bool, dmx_padding) EXTERN_CVAR(Int, snd_volume) EXTERN_CVAR(String, fs_soundfont_path) -EXTERN_CVAR(String, dir_last) EXTERN_CVAR(String, snd_timidity_path) EXTERN_CVAR(String, snd_timidity_options) EXTERN_CVAR(String, snd_midi_player) diff --git a/src/UI/Dialogs/Preferences/BaseResourceArchivesPanel.cpp b/src/UI/Dialogs/Preferences/BaseResourceArchivesPanel.cpp index d3aacf683..7759931de 100644 --- a/src/UI/Dialogs/Preferences/BaseResourceArchivesPanel.cpp +++ b/src/UI/Dialogs/Preferences/BaseResourceArchivesPanel.cpp @@ -40,6 +40,7 @@ #include "UI/Controls/FileLocationPanel.h" #include "UI/WxUtils.h" #include "Utility/Parser.h" +#include "Utility/SFileDialog.h" using namespace slade; @@ -50,7 +51,6 @@ using namespace slade; // // ----------------------------------------------------------------------------- EXTERN_CVAR(Int, base_resource) -EXTERN_CVAR(String, dir_last) EXTERN_CVAR(String, zdoom_pk3_path) @@ -310,34 +310,18 @@ void BaseResourceArchivesPanel::applyPreferences() void BaseResourceArchivesPanel::onBtnAdd(wxCommandEvent& e) { // Create extensions string - wxString extensions = app::archiveManager().getArchiveExtensionsString(); + auto extensions = app::archiveManager().getArchiveExtensionsString(); // Open a file browser dialog that allows multiple selection - wxFileDialog dialog_open( - this, - "Choose file(s) to open", - dir_last, - wxEmptyString, - extensions, - wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST, - wxDefaultPosition); - - // Run the dialog & check that the user didn't cancel - if (dialog_open.ShowModal() == wxID_OK) + filedialog::FDInfo inf; + if (filedialog::openFiles(inf, "Choose file(s) to open", extensions, this)) { - // Get an array of selected filenames - wxArrayString files; - dialog_open.GetPaths(files); - - // Add each to the paths list - for (const auto& file : files) + // Add each file to the paths list + for (const auto& file : inf.filenames) { - if (app::archiveManager().addBaseResourcePath(file.ToStdString())) + if (app::archiveManager().addBaseResourcePath(file)) list_base_archive_paths_->Append(file); } - - // Save 'dir_last' - dir_last = wxutil::strToView(dialog_open.GetDirectory()); } } diff --git a/src/UI/Dialogs/Preferences/PNGPrefsPanel.cpp b/src/UI/Dialogs/Preferences/PNGPrefsPanel.cpp index 2f5c38361..f0c24c707 100644 --- a/src/UI/Dialogs/Preferences/PNGPrefsPanel.cpp +++ b/src/UI/Dialogs/Preferences/PNGPrefsPanel.cpp @@ -46,7 +46,6 @@ using namespace slade; EXTERN_CVAR(String, path_pngout) EXTERN_CVAR(String, path_pngcrush) EXTERN_CVAR(String, path_deflopt) -CVAR(String, dir_last_pngtool, "", CVar::Flag::Save) // ----------------------------------------------------------------------------- diff --git a/src/UI/Dialogs/RunDialog.cpp b/src/UI/Dialogs/RunDialog.cpp index 12f0ba567..17471139a 100644 --- a/src/UI/Dialogs/RunDialog.cpp +++ b/src/UI/Dialogs/RunDialog.cpp @@ -37,8 +37,10 @@ #include "Archive/ArchiveManager.h" #include "General/Executables.h" #include "General/UI.h" +#include "Library/ArchiveRunConfig.h" #include "UI/Controls/ResourceArchiveChooser.h" #include "UI/Controls/SIconButton.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/FileUtils.h" #include "Utility/SFileDialog.h" @@ -56,9 +58,6 @@ using namespace slade; // Variables // // ----------------------------------------------------------------------------- -CVAR(String, run_last_exe, "", CVar::Flag::Save) -CVAR(Int, run_last_config, 0, CVar::Flag::Save) -CVAR(String, run_last_extra, "", CVar::Flag::Save) CVAR(Bool, run_start_3d, false, CVar::Flag::Save) @@ -176,133 +175,67 @@ class RunConfigDialog : public wxDialog // ----------------------------------------------------------------------------- +// IwadSelectionPanel Class // -// RunDialog Class Functions -// -// ----------------------------------------------------------------------------- - - -// ----------------------------------------------------------------------------- -// RunDialog class constructor +// A control to select an IWAD path, with dropdown options showing configured +// base resource archive paths and a button to browse for one // ----------------------------------------------------------------------------- -RunDialog::RunDialog(wxWindow* parent, const Archive* archive, bool show_start_3d_cb, bool run_map) : - SDialog(parent, "Run", "run", 500, 400), - run_map_{ run_map } +class IwadSelectionPanel : public wxPanel { - namespace wx = wxutil; - - // Set dialog icon + title - wx::setWindowIcon(this, "run"); - if (run_map) - SetTitle("Run Map"); - if (archive) - SetTitle(wxString::Format("Run Archive - %s", archive->filename(false))); - - // Setup sizer - auto sizer = new wxBoxSizer(wxVERTICAL); - SetSizer(sizer); - - auto gb_sizer = new wxGridBagSizer(ui::pad(), ui::pad()); - sizer->Add(gb_sizer, wx::sfWithLargeBorder(0, wxLEFT | wxRIGHT | wxTOP).Expand()); - - // Game Executable - gb_sizer->Add( - new wxStaticText(this, -1, "Game Executable:"), wxGBPosition(0, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - choice_game_exes_ = new wxChoice(this, -1); - gb_sizer->Add(choice_game_exes_, wxGBPosition(0, 1), wxGBSpan(1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL); - btn_add_game_ = new SIconButton(this, icons::General, "plus"); - gb_sizer->Add(btn_add_game_, wxGBPosition(0, 3)); - btn_remove_game_ = new SIconButton(this, icons::General, "minus"); - gb_sizer->Add(btn_remove_game_, wxGBPosition(0, 4)); - - // Executable path - gb_sizer->Add(new wxStaticText(this, -1, "Path:"), wxGBPosition(1, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - text_exe_path_ = new wxTextCtrl(this, -1, ""); - // text_exe_path_->Enable(false); - gb_sizer->Add(text_exe_path_, wxGBPosition(1, 1), wxGBSpan(1, 3), wxEXPAND | wxALIGN_CENTER_VERTICAL); - btn_browse_exe_ = new SIconButton(this, icons::General, "open"); - btn_browse_exe_->SetToolTip("Browse..."); - gb_sizer->Add(btn_browse_exe_, wxGBPosition(1, 4)); +public: + IwadSelectionPanel(wxWindow* parent, const string& iwad_path) : wxPanel(parent, -1) + { + auto sizer = new wxBoxSizer(wxHORIZONTAL); + SetSizer(sizer); + combo_iwad_path_ = new wxComboBox(this, -1, iwad_path); + sizer->Add(combo_iwad_path_, wxSizerFlags(1).Expand()); - // Configuration - gb_sizer->Add( - new wxStaticText(this, -1, "Run Configuration:"), wxGBPosition(2, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - choice_config_ = new wxChoice(this, -1); - gb_sizer->Add(choice_config_, wxGBPosition(2, 1), wxDefaultSpan, wxEXPAND | wxALIGN_CENTER_VERTICAL); - btn_edit_config_ = new SIconButton(this, icons::General, "settings"); - btn_edit_config_->SetToolTip("Edit command line"); - gb_sizer->Add(btn_edit_config_, wxGBPosition(2, 2)); - btn_add_config_ = new SIconButton(this, icons::General, "plus"); - gb_sizer->Add(btn_add_config_, wxGBPosition(2, 3)); - btn_remove_config_ = new SIconButton(this, icons::General, "minus"); - btn_remove_config_->Enable(false); - gb_sizer->Add(btn_remove_config_, wxGBPosition(2, 4)); + // Add base resource paths to dropdown + for (const auto& br_path : app::archiveManager().baseResourcePaths()) + combo_iwad_path_->AppendString(br_path); - // Extra parameters - gb_sizer->Add( - new wxStaticText(this, -1, "Extra Parameters:"), wxGBPosition(3, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - text_extra_params_ = new wxTextCtrl(this, -1, run_last_extra); - gb_sizer->Add(text_extra_params_, wxGBPosition(3, 1), wxGBSpan(1, 4), wxEXPAND); + btn_browse_ = new SIconButton(this, "open", "Browse IWAD path"); + sizer->Add(btn_browse_, wxSizerFlags(0).Expand().Border(wxLEFT, ui::pad())); - // Resources - auto frame = new wxStaticBox(this, -1, "Resources"); - auto framesizer = new wxStaticBoxSizer(frame, wxVERTICAL); - sizer->AddSpacer(ui::padLarge()); - sizer->Add(framesizer, wx::sfWithLargeBorder(1, wxLEFT | wxRIGHT).Expand()); - rac_resources_ = new ResourceArchiveChooser(this, archive); - framesizer->Add(rac_resources_, wx::sfWithBorder(1).Expand()); + btn_browse_->Bind( + wxEVT_BUTTON, + [&](wxCommandEvent& e) + { + filedialog::FDInfo inf; + if (filedialog::openFile( + inf, "Browse IWAD path", app::archiveManager().getArchiveExtensionsString(), this)) + { + combo_iwad_path_->SetValue(inf.filenames[0]); + } + }); + } - // Start from 3d mode camera - auto hbox = new wxBoxSizer(wxHORIZONTAL); - sizer->AddSpacer(ui::padLarge()); - sizer->Add(hbox, wx::sfWithLargeBorder(0, wxLEFT | wxRIGHT | wxBOTTOM).Expand()); - cb_start_3d_ = new wxCheckBox(this, -1, "Start from 3D mode camera position"); - cb_start_3d_->SetValue(run_start_3d); - if (show_start_3d_cb) - hbox->Add(cb_start_3d_, wxSizerFlags().CenterVertical()); - else - cb_start_3d_->Show(false); + string selectedIwadPath() const { return combo_iwad_path_->GetValue().ToStdString(); } - // Dialog buttons - btn_run_ = new wxButton(this, wxID_OK, "Run"); - btn_run_->SetDefault(); - btn_cancel_ = new wxButton(this, wxID_CANCEL, "Cancel"); - hbox->Add(wx::createDialogButtonBox(btn_run_, btn_cancel_), wxSizerFlags(1).Expand()); +private: + wxComboBox* combo_iwad_path_ = nullptr; + SIconButton* btn_browse_ = nullptr; +}; - // Populate game executables dropdown - int last_index = -1; - for (unsigned a = 0; a < executables::nGameExes(); a++) - { - auto exe = executables::gameExe(a); - choice_game_exes_->AppendString(exe->name); - if (exe->id == run_last_exe) - last_index = choice_game_exes_->GetCount() - 1; - } - if (static_cast(choice_game_exes_->GetCount()) > last_index) - { - choice_game_exes_->Select(last_index); - openGameExe(last_index); - choice_config_->Select(run_last_config); - } +// ----------------------------------------------------------------------------- +// +// RunDialog Class Functions +// +// ----------------------------------------------------------------------------- - // Bind Events - btn_add_game_->Bind(wxEVT_BUTTON, &RunDialog::onBtnAddGame, this); - btn_remove_game_->Bind(wxEVT_BUTTON, &RunDialog::onBtnRemoveGame, this); - btn_browse_exe_->Bind(wxEVT_BUTTON, &RunDialog::onBtnBrowseExe, this); - btn_edit_config_->Bind(wxEVT_BUTTON, &RunDialog::onBtnEditConfig, this); - btn_add_config_->Bind(wxEVT_BUTTON, &RunDialog::onBtnAddConfig, this); - btn_remove_config_->Bind(wxEVT_BUTTON, &RunDialog::onBtnRemoveConfig, this); - btn_run_->Bind(wxEVT_BUTTON, &RunDialog::onBtnRun, this); - btn_cancel_->Bind(wxEVT_BUTTON, &RunDialog::onBtnCancel, this); - choice_game_exes_->Bind(wxEVT_CHOICE, &RunDialog::onChoiceGameExe, this); - choice_config_->Bind(wxEVT_CHOICE, &RunDialog::onChoiceConfig, this); - gb_sizer->AddGrowableCol(1, 1); - wxTopLevelWindowBase::SetMinSize(wxSize(ui::scalePx(500), ui::scalePx(400))); - wxWindowBase::Layout(); - CenterOnParent(); - btn_run_->SetFocusFromKbd(); +// ----------------------------------------------------------------------------- +// RunDialog class constructor(s) +// ----------------------------------------------------------------------------- +RunDialog::RunDialog(wxWindow* parent, Archive* archive, bool show_start_3d_cb, bool run_map) : + SDialog(parent, "Run", "run", 500, 400), run_map_{ run_map } +{ + setup(archive, archive ? archive->libraryId() : -1, show_start_3d_cb, run_map); +} +RunDialog::RunDialog(wxWindow* parent, int64_t archive_lib_id) : SDialog(parent, "Run", "run", 500, 400) +{ + setup(nullptr, archive_lib_id); } // ----------------------------------------------------------------------------- @@ -349,8 +282,7 @@ void RunDialog::openGameExe(unsigned index) const // Returns a command line based on the currently selected run configuration and // resources // ----------------------------------------------------------------------------- -wxString RunDialog::selectedCommandLine(const Archive* archive, const wxString& map_name, const wxString& map_file) - const +wxString RunDialog::selectedCommandLine(const Config& cfg) const { auto exe = executables::gameExe(choice_game_exes_->GetSelection()); if (exe) @@ -363,34 +295,36 @@ wxString RunDialog::selectedCommandLine(const Archive* archive, const wxString& wxString path = wxString::Format("\"%s\"", exe_path); - unsigned cfg = choice_config_->GetSelection(); - const auto& configs = run_map_ ? exe->map_configs : exe->run_configs; - if (cfg < configs.size()) + unsigned cfg_index = choice_config_->GetSelection(); + const auto& configs = run_map_ ? exe->map_configs : exe->run_configs; + if (cfg_index < configs.size()) { path += " "; - path += configs[cfg].second; + path += configs[cfg_index].second; } // IWAD - auto bra = app::archiveManager().baseResourceArchive(); - path.Replace("%i", wxString::Format("\"%s\"", bra ? bra->filename() : "")); + auto iwad_path = cfg.iwad_path; + if (iwad_path.IsEmpty()) + iwad_path = dynamic_cast(isp_iwad_)->selectedIwadPath(); + path.Replace("%i", wxString::Format("\"%s\"", iwad_path)); // Resources path.Replace("%r", selectedResourceList()); // Archive (+ temp map if specified) - if (map_file.IsEmpty() && archive) - path.Replace("%a", wxString::Format("\"%s\"", archive->filename())); + if (cfg.map_file.IsEmpty() && !cfg.archive_path.IsEmpty()) + path.Replace("%a", wxString::Format("\"%s\"", cfg.archive_path)); else { - if (archive) - path.Replace("%a", wxString::Format("\"%s\" \"%s\"", archive->filename(), map_file)); + if (!cfg.archive_path.empty()) + path.Replace("%a", wxString::Format("\"%s\" \"%s\"", cfg.archive_path, cfg.map_file)); else - path.Replace("%a", wxString::Format("\"%s\"", map_file)); + path.Replace("%a", wxString::Format("\"%s\"", cfg.map_file)); } // Running an archive yields no map name, so don't try to warp - if (map_name.IsEmpty()) + if (cfg.map_name.IsEmpty()) { path.Replace("-warp ", wxEmptyString); path.Replace("+map ", wxEmptyString); @@ -400,12 +334,12 @@ wxString RunDialog::selectedCommandLine(const Archive* archive, const wxString& // Map name else { - path.Replace("%mn", map_name); + path.Replace("%mn", cfg.map_name); // Map warp if (path.Contains("%mw")) { - wxString mn = map_name.Lower(); + wxString mn = cfg.map_name.Lower(); // MAPxx wxString mapnum; @@ -413,7 +347,7 @@ wxString RunDialog::selectedCommandLine(const Archive* archive, const wxString& path.Replace("%mw", mapnum); // ExMx - else if (map_name.length() == 4 && mn[0] == 'e' && mn[2] == 'm') + else if (cfg.map_name.length() == 4 && mn[0] == 'e' && mn[2] == 'm') path.Replace("%mw", wxString::Format("%c %c", mn[1], mn[3])); } } @@ -475,6 +409,172 @@ bool RunDialog::start3dModeChecked() const return cb_start_3d_->GetValue(); } +// ----------------------------------------------------------------------------- +// Runs the currently selected executable+run configuration with [archive_path] +// ----------------------------------------------------------------------------- +void RunDialog::run(const Config& cfg, int64_t archive_lib_id) const +{ + auto command = selectedCommandLine(cfg); + if (!command.IsEmpty()) + { + // Save run config for archive + if (archive_lib_id >= 0) + { + library::ArchiveRunConfigRow run_cfg{ archive_lib_id }; + run_cfg.executable_id = wxutil::strToView(selectedExeId()); + run_cfg.run_config = choice_config_->GetSelection(); + run_cfg.run_extra = wxutil::strToView(text_extra_params_->GetValue()); + run_cfg.iwad_path = dynamic_cast(isp_iwad_)->selectedIwadPath(); + library::saveArchiveRunConfig(run_cfg); + } + + // Set working directory + wxString wd = wxGetCwd(); + wxSetWorkingDirectory(selectedExeDir()); + + // Run + wxExecute(command, wxEXEC_ASYNC); + + // Restore working directory + wxSetWorkingDirectory(wd); + } +} + +// ----------------------------------------------------------------------------- +// Setup the dialog layout and controls +// ----------------------------------------------------------------------------- +void RunDialog::setup(Archive* archive, int64_t archive_lib_id, bool show_start_3d_cb, bool run_map) +{ + // Set dialog icon + wxutil::setWindowIcon(this, "run"); + + // Get initial values + string run_last_extra = ui::getStateString("RunDialogLastExtra"); + string run_last_exe = ui::getStateString("RunDialogLastExe"); + int run_last_cfg = ui::getStateInt("RunDialogLastConfig"); + string iwad_path = app::archiveManager().currentBaseResourcePath(); + if (archive_lib_id >= 0) + { + auto run_cfg = library::getArchiveRunConfig(archive_lib_id); + if (run_cfg.archive_id >= 0) + { + run_last_exe = run_cfg.executable_id; + run_last_cfg = run_cfg.run_config; + run_last_extra = run_cfg.run_extra; + iwad_path = run_cfg.iwad_path; + } + } + + // Setup sizer + auto sizer = new wxBoxSizer(wxVERTICAL); + SetSizer(sizer); + + auto gb_sizer = new wxGridBagSizer(ui::pad(), ui::pad()); + sizer->Add(gb_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, ui::padLarge()); + + // Game Executable + gb_sizer->Add( + new wxStaticText(this, -1, "Game Executable:"), wxGBPosition(0, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + choice_game_exes_ = new wxChoice(this, -1); + gb_sizer->Add(choice_game_exes_, wxGBPosition(0, 1), wxGBSpan(1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL); + btn_add_game_ = new SIconButton(this, icons::General, "plus"); + gb_sizer->Add(btn_add_game_, wxGBPosition(0, 3)); + btn_remove_game_ = new SIconButton(this, icons::General, "minus"); + gb_sizer->Add(btn_remove_game_, wxGBPosition(0, 4)); + + // Executable path + gb_sizer->Add(new wxStaticText(this, -1, "Path:"), wxGBPosition(1, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + text_exe_path_ = new wxTextCtrl(this, -1, ""); + // text_exe_path_->Enable(false); + gb_sizer->Add(text_exe_path_, wxGBPosition(1, 1), wxGBSpan(1, 3), wxEXPAND | wxALIGN_CENTER_VERTICAL); + btn_browse_exe_ = new SIconButton(this, icons::General, "open"); + btn_browse_exe_->SetToolTip("Browse..."); + gb_sizer->Add(btn_browse_exe_, wxGBPosition(1, 4)); + + // Configuration + gb_sizer->Add( + new wxStaticText(this, -1, "Run Configuration:"), wxGBPosition(2, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + choice_config_ = new wxChoice(this, -1); + gb_sizer->Add(choice_config_, wxGBPosition(2, 1), wxDefaultSpan, wxEXPAND | wxALIGN_CENTER_VERTICAL); + btn_edit_config_ = new SIconButton(this, icons::General, "settings"); + btn_edit_config_->SetToolTip("Edit command line"); + gb_sizer->Add(btn_edit_config_, wxGBPosition(2, 2)); + btn_add_config_ = new SIconButton(this, icons::General, "plus"); + gb_sizer->Add(btn_add_config_, wxGBPosition(2, 3)); + btn_remove_config_ = new SIconButton(this, icons::General, "minus"); + btn_remove_config_->Enable(false); + gb_sizer->Add(btn_remove_config_, wxGBPosition(2, 4)); + + // Extra parameters + gb_sizer->Add( + new wxStaticText(this, -1, "Extra Parameters:"), wxGBPosition(3, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + text_extra_params_ = new wxTextCtrl(this, -1, run_last_extra); + gb_sizer->Add(text_extra_params_, wxGBPosition(3, 1), wxGBSpan(1, 4), wxEXPAND); + + // Resources + auto frame = new wxStaticBox(this, -1, "Resources"); + auto framesizer = new wxStaticBoxSizer(frame, wxVERTICAL); + sizer->AddSpacer(ui::padLarge()); + sizer->Add(framesizer, 1, wxEXPAND | wxLEFT | wxRIGHT, ui::padLarge()); + isp_iwad_ = new IwadSelectionPanel(this, iwad_path); + framesizer->Add( + wxutil::createLabelHBox(this, "IWAD:", isp_iwad_), 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, ui::pad()); + rac_resources_ = new ResourceArchiveChooser(this, archive); + framesizer->Add(rac_resources_, 1, wxEXPAND | wxALL, ui::pad()); + + // Start from 3d mode camera + auto hbox = new wxBoxSizer(wxHORIZONTAL); + sizer->AddSpacer(ui::padLarge()); + sizer->Add(hbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, ui::padLarge()); + cb_start_3d_ = new wxCheckBox(this, -1, "Start from 3D mode camera position"); + cb_start_3d_->SetValue(run_start_3d); + if (show_start_3d_cb) + hbox->Add(cb_start_3d_, 0, wxALIGN_CENTER_VERTICAL); + else + cb_start_3d_->Show(false); + + // Dialog buttons + btn_run_ = new wxButton(this, wxID_OK, "Run"); + btn_run_->SetDefault(); + btn_cancel_ = new wxButton(this, wxID_CANCEL, "Cancel"); + hbox->Add(wxutil::createDialogButtonBox(btn_run_, btn_cancel_), 1, wxEXPAND); + + // Populate game executables dropdown + int last_index = -1; + for (unsigned a = 0; a < executables::nGameExes(); a++) + { + auto exe = executables::gameExe(a); + choice_game_exes_->AppendString(exe->name); + + if (exe->id == run_last_exe) + last_index = choice_game_exes_->GetCount() - 1; + } + if ((int)choice_game_exes_->GetCount() > last_index) + { + choice_game_exes_->Select(last_index); + openGameExe(last_index); + choice_config_->Select(run_last_cfg); + } + + // Bind Events + btn_add_game_->Bind(wxEVT_BUTTON, &RunDialog::onBtnAddGame, this); + btn_remove_game_->Bind(wxEVT_BUTTON, &RunDialog::onBtnRemoveGame, this); + btn_browse_exe_->Bind(wxEVT_BUTTON, &RunDialog::onBtnBrowseExe, this); + btn_edit_config_->Bind(wxEVT_BUTTON, &RunDialog::onBtnEditConfig, this); + btn_add_config_->Bind(wxEVT_BUTTON, &RunDialog::onBtnAddConfig, this); + btn_remove_config_->Bind(wxEVT_BUTTON, &RunDialog::onBtnRemoveConfig, this); + btn_run_->Bind(wxEVT_BUTTON, &RunDialog::onBtnRun, this); + btn_cancel_->Bind(wxEVT_BUTTON, &RunDialog::onBtnCancel, this); + choice_game_exes_->Bind(wxEVT_CHOICE, &RunDialog::onChoiceGameExe, this); + choice_config_->Bind(wxEVT_CHOICE, &RunDialog::onChoiceConfig, this); + + gb_sizer->AddGrowableCol(1, 1); + SetMinSize(wxSize(ui::scalePx(500), ui::scalePx(400))); + Layout(); + CenterOnParent(); + btn_run_->SetFocusFromKbd(); +} + // ----------------------------------------------------------------------------- // @@ -568,10 +668,10 @@ void RunDialog::onBtnEditConfig(wxCommandEvent& e) RunConfigDialog dlg(this, "Edit Run Config", name, params, custom); if (dlg.ShowModal() == wxID_OK) { - wxString newname = dlg.name().IsEmpty() ? wxString(configs[index].first) : dlg.name(); - configs[index].first = newname; + wxString cfg_name = dlg.name().IsEmpty() ? wxString(configs[index].first) : dlg.name(); + configs[index].first = cfg_name; configs[index].second = dlg.params(); - choice_config_->SetString(index, newname); + choice_config_->SetString(index, cfg_name); } } @@ -592,10 +692,10 @@ void RunDialog::onBtnRun(wxCommandEvent& e) auto exe = executables::gameExe(choice_game_exes_->GetSelection()); exe->path = exe_path; - // Update cvars - run_last_extra = wxutil::strToView(text_extra_params_->GetValue()); - run_last_config = choice_config_->GetSelection(); - run_last_exe = wxutil::strToView(selectedExeId()); + // Update saved UI state + ui::saveStateString("RunDialogLastExtra", wxutil::strToView(text_extra_params_->GetValue())); + ui::saveStateInt("RunDialogLastConfig", choice_config_->GetSelection()); + ui::saveStateString("RunDialogLastExe", wxutil::strToView(selectedExeId())); EndModal(wxID_OK); } @@ -605,10 +705,10 @@ void RunDialog::onBtnRun(wxCommandEvent& e) // ----------------------------------------------------------------------------- void RunDialog::onBtnCancel(wxCommandEvent& e) { - // Update cvars - run_last_extra = wxutil::strToView(text_extra_params_->GetValue()); - run_last_config = choice_config_->GetSelection(); - run_last_exe = wxutil::strToView(selectedExeId()); + // Update saved UI state + ui::saveStateString("RunDialogLastExtra", wxutil::strToView(text_extra_params_->GetValue())); + ui::saveStateInt("RunDialogLastConfig", choice_config_->GetSelection()); + ui::saveStateString("RunDialogLastExe", wxutil::strToView(selectedExeId())); EndModal(wxID_CANCEL); } @@ -619,7 +719,7 @@ void RunDialog::onBtnCancel(wxCommandEvent& e) void RunDialog::onChoiceGameExe(wxCommandEvent& e) { openGameExe(e.GetSelection()); - run_last_exe = wxutil::strToView(selectedExeId()); + ui::saveStateString("RunDialogLastExe", wxutil::strToView(selectedExeId())); } // ----------------------------------------------------------------------------- @@ -627,7 +727,7 @@ void RunDialog::onChoiceGameExe(wxCommandEvent& e) // ----------------------------------------------------------------------------- void RunDialog::onChoiceConfig(wxCommandEvent& e) { - run_last_config = choice_config_->GetSelection(); + ui::saveStateInt("RunDialogLastConfig", choice_config_->GetSelection()); btn_edit_config_->Enable(true); auto exe = executables::gameExe(choice_game_exes_->GetSelection()); const auto& configs_custom = run_map_ ? exe->map_configs_custom : exe->run_configs_custom; diff --git a/src/UI/Dialogs/RunDialog.h b/src/UI/Dialogs/RunDialog.h index 2f215fe2b..2cb8bfcae 100644 --- a/src/UI/Dialogs/RunDialog.h +++ b/src/UI/Dialogs/RunDialog.h @@ -9,15 +9,27 @@ class ResourceArchiveChooser; class RunDialog : public SDialog { public: - RunDialog(wxWindow* parent, const Archive* archive, bool show_start_3d_cb = false, bool run_map = false); + RunDialog(wxWindow* parent, Archive* archive, bool show_start_3d_cb = false, bool run_map = false); + RunDialog(wxWindow* parent, int64_t archive_lib_id); ~RunDialog() override; + struct Config + { + wxString archive_path; + wxString iwad_path; + wxString map_name; + wxString map_file; + + Config(const string& archive_path) : archive_path{ archive_path } {} + }; + void openGameExe(unsigned index) const; - wxString selectedCommandLine(const Archive* archive, const wxString& map_name, const wxString& map_file = "") const; + wxString selectedCommandLine(const Config& cfg) const; wxString selectedResourceList() const; wxString selectedExeDir() const; wxString selectedExeId() const; bool start3dModeChecked() const; + void run(const Config& cfg, int64_t archive_lib_id = -1) const; private: wxChoice* choice_game_exes_ = nullptr; @@ -34,8 +46,15 @@ class RunDialog : public SDialog ResourceArchiveChooser* rac_resources_ = nullptr; wxTextCtrl* text_extra_params_ = nullptr; wxCheckBox* cb_start_3d_ = nullptr; + wxWindow* isp_iwad_ = nullptr; bool run_map_ = false; + void setup( + Archive* archive = nullptr, + int64_t archive_lib_id = -1, + bool show_start_3d_cb = false, + bool run_map = false); + // Events void onBtnAddGame(wxCommandEvent& e); void onBtnBrowseExe(wxCommandEvent& e); diff --git a/src/UI/Lists/ArchiveEntryTree.cpp b/src/UI/Lists/ArchiveEntryTree.cpp index c531bfafb..64fbd0db9 100644 --- a/src/UI/Lists/ArchiveEntryTree.cpp +++ b/src/UI/Lists/ArchiveEntryTree.cpp @@ -44,7 +44,9 @@ #include "General/UI.h" #include "General/UndoRedo.h" #include "Graphics/Icons.h" +#include "Library/ArchiveUIConfig.h" #include "UI/SToolBar/SToolBarButton.h" +#include "UI/State.h" #include "UI/WxUtils.h" #include "Utility/StringUtils.h" #include @@ -59,25 +61,12 @@ using namespace ui; // ----------------------------------------------------------------------------- namespace slade::ui { -wxColour col_text_modified(0, 0, 0, 0); -wxColour col_text_new(0, 0, 0, 0); -wxColour col_text_locked(0, 0, 0, 0); -#if wxCHECK_VERSION(3, 1, 6) -std::unordered_map icon_cache; -#else -std::unordered_map icon_cache; -#endif -vector elist_chars = { - '.', ',', '_', '-', '+', '=', '`', '~', '!', '@', '#', '$', '(', ')', '[', - ']', '{', '}', ':', ';', '/', '\\', '<', '>', '?', '^', '&', '\'', '\"', -}; +wxColour col_text_modified(0, 0, 0, 0); +wxColour col_text_new(0, 0, 0, 0); +wxColour col_text_locked(0, 0, 0, 0); +icons::IconCache icon_cache; } // namespace slade::ui -CVAR(Int, elist_colsize_name_tree, 150, CVar::Save) -CVAR(Int, elist_colsize_name_list, 150, CVar::Save) -CVAR(Int, elist_colsize_size, 80, CVar::Save) -CVAR(Int, elist_colsize_type, 150, CVar::Save) -CVAR(Int, elist_colsize_index, 50, CVar::Save) #ifdef __WXGTK__ // Disable by default in GTK because double-click seems to trigger it, which interferes // with double-click to expand folders or open entries @@ -381,29 +370,18 @@ void ArchiveViewModel::GetValue(wxVariant& variant, const wxDataViewItem& item, if (col == 0) { // Find icon in cache - if (icon_cache.find(entry->type()->icon()) == icon_cache.end()) + if (!icon_cache.isCached(entry->type()->icon())) { // Not found, add to cache const auto pad = Point2i{ 1, elist_icon_padding }; - -#if wxCHECK_VERSION(3, 1, 6) - const auto bundle = icons::getIcon(icons::Type::Entry, entry->type()->icon(), elist_icon_size, pad); - icon_cache[entry->type()->icon()] = bundle; -#else - const auto size = scalePx(elist_icon_size); - const auto bmp = icons::getIcon(icons::Type::Entry, entry->type()->icon(), size, pad); - - wxIcon icon; - icon.CopyFromBitmap(bmp); - icon_cache[entry->type()->icon()] = icon; -#endif + icon_cache.cacheIcon(icons::Type::Entry, entry->type()->icon(), elist_icon_size, pad); } wxString name = entry->name(); if (modified_indicator_ && entry->state() != EntryState::Unmodified) - variant << wxDataViewIconText(entry->name() + " *", icon_cache[entry->type()->icon()]); + variant << wxDataViewIconText(entry->name() + " *", icon_cache.icons[entry->type()->icon()]); else - variant << wxDataViewIconText(entry->name(), icon_cache[entry->type()->icon()]); + variant << wxDataViewIconText(entry->name(), icon_cache.icons[entry->type()->icon()]); } // Size column @@ -738,7 +716,7 @@ int ArchiveViewModel::Compare( else { // Directory archives default to alphabetical order - if (const auto archive = archive_.lock(); archive->format() == ArchiveFormat::Dir) + if (const auto archive = archive_.lock(); archive->formatId() == "folder") cmpval = e1->upperName().compare(e2->upperName()); // Everything else defaults to index order @@ -876,7 +854,7 @@ ArchiveEntryTree::ArchiveEntryTree( const shared_ptr& archive, UndoManager* undo_manager, bool force_list) : - wxDataViewCtrl(parent, -1, wxDefaultPosition, wxDefaultSize, wxDV_MULTIPLE), + SDataViewCtrl{ parent, wxDV_MULTIPLE }, archive_{ archive } { // Init settings @@ -926,9 +904,6 @@ ArchiveEntryTree::ArchiveEntryTree( e.Skip(); }); - // Update column width cvars when we can - Bind(wxEVT_IDLE, [this](wxIdleEvent&) { saveColumnWidths(); }); - // Disable modified indicator (" *" after name) when in-place editing entry names Bind( wxEVT_DATAVIEW_ITEM_EDITING_STARTED, @@ -952,6 +927,15 @@ ArchiveEntryTree::ArchiveEntryTree( model_->showModifiedIndicators(true); }); + // Header left click (ie. sorting change) + Bind( + wxEVT_DATAVIEW_COLUMN_HEADER_CLICK, + [this](wxDataViewEvent& e) + { + CallAfter(&ArchiveEntryTree::saveColumnConfig); + e.Skip(); + }); + // Header right click Bind( wxEVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK, @@ -961,9 +945,9 @@ ArchiveEntryTree::ArchiveEntryTree( wxMenu context; context.Append(0, "Reset Sorting"); context.AppendSeparator(); - context.AppendCheckItem(1, "Index", "Show the Index column")->Check(elist_colindex_show); - context.AppendCheckItem(2, "Size", "Show the Size column")->Check(elist_colsize_show); - context.AppendCheckItem(3, "Type", "Show the Type column")->Check(elist_coltype_show); + appendColumnToggleItem(context, 3); // Index + appendColumnToggleItem(context, 1); // Size + appendColumnToggleItem(context, 2); // Type PopupMenu(&context); e.Skip(); }); @@ -989,114 +973,36 @@ ArchiveEntryTree::ArchiveEntryTree( col_index_->UnsetAsSortKey(); #endif model_->Resort(); + saveColumnConfig(); + wxDataViewEvent de; de.SetEventType(wxEVT_DATAVIEW_COLUMN_SORTED); ProcessWindowEvent(de); } - else if (e.GetId() == 1) + else if (e.GetId() == 3) { // Toggle index column - elist_colindex_show = !elist_colindex_show; - col_index_->SetHidden(!elist_colindex_show); + toggleColumnVisibility(3, "EntryListIndexVisible"); updateColumnWidths(); + saveColumnConfig(); } - else if (e.GetId() == 2) + else if (e.GetId() == 1) { // Toggle size column - elist_colsize_show = !elist_colsize_show; - col_size_->SetHidden(!elist_colsize_show); + toggleColumnVisibility(1, "EntryListSizeVisible"); updateColumnWidths(); + saveColumnConfig(); } - else if (e.GetId() == 3) + else if (e.GetId() == 2) { // Toggle type column - elist_coltype_show = !elist_coltype_show; - col_type_->SetHidden(!elist_coltype_show); + toggleColumnVisibility(2, "EntryListTypeVisible"); updateColumnWidths(); + saveColumnConfig(); } else e.Skip(); }); - -#ifdef __WXMSW__ - // Keypress event - Bind( - wxEVT_CHAR, - [this](wxKeyEvent& e) - { - // Custom handling for shift+up/down - if (e.ShiftDown()) - { - int from_row = multi_select_base_index_; - - // Get row to select to - // TODO: Handle PgUp/PgDn as well? - int to_row; - switch (e.GetKeyCode()) - { - case WXK_DOWN: to_row = GetRowByItem(GetCurrentItem()) + 1; break; - case WXK_UP: to_row = GetRowByItem(GetCurrentItem()) - 1; break; - default: - // Not up or down arrow, do default handling - e.Skip(); - return; - } - - // Get new item to focus - auto new_current_item = GetItemByRow(to_row); - if (!new_current_item.IsOk()) - { - e.Skip(); - return; - } - - // Ensure valid range - if (from_row > to_row) - std::swap(from_row, to_row); - - // Get items to select - wxDataViewItemArray items; - for (int i = from_row; i <= to_row; ++i) - items.Add(GetItemByRow(i)); - - // Set new selection - SetSelections(items); - SetCurrentItem(new_current_item); - - // Trigger selection change event - wxDataViewEvent de; - de.SetEventType(wxEVT_DATAVIEW_SELECTION_CHANGED); - ProcessWindowEvent(de); - - return; - } - - // Search - if (e.GetModifiers() == 0) - { - if (searchChar(e.GetKeyCode())) - return; - } - - e.Skip(); - }); - - Bind( - wxEVT_DATAVIEW_SELECTION_CHANGED, - [this](wxDataViewEvent& e) - { - if (GetSelectedItemsCount() == 1) - multi_select_base_index_ = GetRowByItem(GetSelection()); - - // Clear search string if selection change wasn't a result of searching - if (e.GetString().Cmp("search")) - search_.clear(); - - e.Skip(); - }); - - Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { search_.clear(); }); -#endif } // ----------------------------------------------------------------------------- @@ -1502,78 +1408,75 @@ void ArchiveEntryTree::setupColumns() if (!archive) return; + // Name column for searching + setSearchColumn(0); + auto colstyle_visible = wxDATAVIEW_COL_SORTABLE | wxDATAVIEW_COL_RESIZABLE; auto colstyle_hidden = colstyle_visible | wxDATAVIEW_COL_HIDDEN; + // Get entry list config from library for the archive + auto config = library::getArchiveUIConfig(archive->libraryId()); + + // If no config exists for the archive, create one from the cvars + if (config.archive_id < 0) + { + config = library::ArchiveUIConfigRow{ archive->libraryId(), archive->formatInfo().supports_dirs }; + library::saveArchiveUIConfig(config); + } + // Add Columns col_index_ = AppendTextColumn( "#", 3, wxDATAVIEW_CELL_INERT, - elist_colsize_index, + config.elist_index_width, wxALIGN_NOT, - elist_colindex_show ? colstyle_visible : colstyle_hidden); + config.elist_index_visible ? colstyle_visible : colstyle_hidden); col_name_ = AppendIconTextColumn( "Name", 0, elist_rename_inplace ? wxDATAVIEW_CELL_EDITABLE : wxDATAVIEW_CELL_INERT, - model_->viewType() == ArchiveViewModel::ViewType::Tree ? elist_colsize_name_tree : elist_colsize_name_list, + config.elist_name_width, wxALIGN_NOT, colstyle_visible); col_size_ = AppendTextColumn( "Size", 1, wxDATAVIEW_CELL_INERT, - elist_colsize_size, + config.elist_size_width, wxALIGN_NOT, - elist_colsize_show ? colstyle_visible : colstyle_hidden); + config.elist_size_visible ? colstyle_visible : colstyle_hidden); col_type_ = AppendTextColumn( "Type", 2, wxDATAVIEW_CELL_INERT, - elist_colsize_type, + config.elist_type_width, wxALIGN_NOT, - elist_coltype_show ? colstyle_visible : colstyle_hidden); + config.elist_type_visible ? colstyle_visible : colstyle_hidden); SetExpanderColumn(col_name_); // Last column will expand anyway, this ensures we don't get unnecessary horizontal scrollbars GetColumn(GetColumnCount() - 1)->SetWidth(0); -} -// ----------------------------------------------------------------------------- -// Saves the current column widths to their respective cvars -// ----------------------------------------------------------------------------- -void ArchiveEntryTree::saveColumnWidths() const -{ - // Get the last visible column (we don't want to save the width of this column since it stretches) - wxDataViewColumn* last_col = nullptr; - for (int i = static_cast(GetColumnCount()) - 1; i >= 0; --i) - if (!GetColumn(i)->IsHidden()) - { - last_col = GetColumn(i); - break; - } - - if (last_col != col_name_) + // Load sorting config + if (!config.elist_sort_column.empty()) { - if (model_->viewType() == ArchiveViewModel::ViewType::Tree) - elist_colsize_name_tree = col_name_->GetWidth(); - else - elist_colsize_name_list = col_name_->GetWidth(); + if (config.elist_sort_column == "index") + col_index_->SetSortOrder(!config.elist_sort_descending); + else if (config.elist_sort_column == "name") + col_name_->SetSortOrder(!config.elist_sort_descending); + else if (config.elist_sort_column == "size") + col_size_->SetSortOrder(!config.elist_sort_descending); + else if (config.elist_sort_column == "type") + col_type_->SetSortOrder(!config.elist_sort_descending); + + model_->Resort(); } - - if (last_col != col_size_ && !col_size_->IsHidden()) - elist_colsize_size = col_size_->GetWidth(); - - if (last_col != col_type_ && !col_type_->IsHidden()) - elist_colsize_type = col_type_->GetWidth(); - - if (!col_index_->IsHidden()) - elist_colsize_index = col_index_->GetWidth(); } // ----------------------------------------------------------------------------- -// Updates the currently visible columns' widths from their respective cvars +// Updates the currently visible columns' widths from their respective cvars or +// the config in the library // ----------------------------------------------------------------------------- void ArchiveEntryTree::updateColumnWidths() { @@ -1590,122 +1493,104 @@ void ArchiveEntryTree::updateColumnWidths() break; } + auto config = library::getArchiveUIConfig(archive->libraryId()); + Freeze(); - col_index_->SetWidth(elist_colsize_index); - col_name_->SetWidth( - model_->viewType() == ArchiveViewModel::ViewType::Tree ? elist_colsize_name_tree : elist_colsize_name_list); - col_size_->SetWidth(col_size_ == last_col ? 0 : elist_colsize_size); - col_type_->SetWidth(col_type_ == last_col ? 0 : elist_colsize_type); + col_index_->SetWidth(config.elist_index_width); + col_name_->SetWidth(col_name_ == last_col ? 0 : config.elist_name_width); + col_size_->SetWidth(col_size_ == last_col ? 0 : config.elist_size_width); + col_type_->SetWidth(col_type_ == last_col ? 0 : config.elist_type_width); Thaw(); } -#ifdef __WXMSW__ -// ----------------------------------------------------------------------------- -// Beginning from [index_start], finds and selects the first entry with a name -// matching the internal search_ string. -// Returns true if a match was found -// ----------------------------------------------------------------------------- -bool ArchiveEntryTree::lookForSearchEntryFrom(int index_start) +void ArchiveEntryTree::saveColumnConfig() // (can't be made const - is called from wx event handler) { - long index = index_start; - wxVariant value; + const auto archive = archive_.lock(); + if (!archive) + return; - while (true) - { - auto item = GetItemByRow(index); + auto config = library::getArchiveUIConfig(archive->libraryId()); + if (config.archive_id < 0) + return; - if (auto* entry = static_cast(item.GetID())) - { - if (strutil::startsWithCI(entry->name(), search_)) - { - // Matches, update selection+focus - wxDataViewItemArray items; - items.Add(item); - SetSelections(items); - SetCurrentItem(item); - EnsureVisible(item); - return true; - } + // Visible columns + config.elist_index_visible = col_index_->IsShown(); + config.elist_size_visible = col_size_->IsShown(); + config.elist_type_visible = col_type_->IsShown(); - ++index; - } - else - break; + // Sorting + config.elist_sort_descending = false; + if (col_index_->IsSortKey()) + { + config.elist_sort_column = "index"; + config.elist_sort_descending = !col_index_->IsSortOrderAscending(); + } + else if (col_name_->IsSortKey()) + { + config.elist_sort_column = "name"; + config.elist_sort_descending = !col_name_->IsSortOrderAscending(); + } + else if (col_size_->IsSortKey()) + { + config.elist_sort_column = "size"; + config.elist_sort_descending = !col_size_->IsSortOrderAscending(); + } + else if (col_type_->IsSortKey()) + { + config.elist_sort_column = "type"; + config.elist_sort_descending = !col_type_->IsSortOrderAscending(); } + else + config.elist_sort_column = {}; - // Didn't get any match - return false; + library::saveArchiveUIConfig(config); } -// ----------------------------------------------------------------------------- -// Adds [key_code] to the current internal search string (if valid) and performs -// quick search. -// Returns false if the key was not a 'real' character usable for searching -// ----------------------------------------------------------------------------- -bool ArchiveEntryTree::searchChar(int key_code) +void ArchiveEntryTree::onAnyColumnResized() { - // Check the key pressed is actually a character (a-z, 0-9 etc) - bool real_char = false; - if (key_code >= 'a' && key_code <= 'z') // Lowercase - real_char = true; - else if (key_code >= 'A' && key_code <= 'Z') // Uppercase - real_char = true; - else if (key_code >= '0' && key_code <= '9') // Number - real_char = true; - else + const auto archive = archive_.lock(); + if (!archive) + return; + + auto config = library::getArchiveUIConfig(archive->libraryId()); + if (config.archive_id < 0) + return; + + // Get the last visible column (we don't want to save the width of this column since it stretches) + auto last_col = lastVisibleColumn(); + + // Index + if (col_index_->IsShown()) { - for (int elist_char : elist_chars) - { - if (key_code == elist_char) - { - real_char = true; - break; - } - } + config.elist_index_width = col_index_->GetWidth(); + saveStateInt("EntryListIndexWidth", config.elist_index_width); } - if (!real_char) + // Name + if (col_name_ != last_col) { - search_.clear(); - return false; + config.elist_name_width = col_name_->GetWidth(); + saveStateInt( + archive->formatInfo().supports_dirs ? "EntryListNameWidthTree" : "EntryListNameWidthList", + config.elist_name_width); } - // Get currently focused item (or first if nothing is focused) - auto index = GetRowByItem(GetCurrentItem()); - if (index < 0) - index = 0; - - // Build search string - search_ += static_cast(key_code); - - // Find matching entry/dir, beginning from current item - // If no match found, try again from the top - auto found = true; - if (!lookForSearchEntryFrom(index)) - found = lookForSearchEntryFrom(0); - - // No match, continue from next item with fresh search string - if (!found) + // Size + if (col_size_ != last_col && col_size_->IsShown()) { - search_.clear(); - search_ += static_cast(key_code); - found = lookForSearchEntryFrom(index + 1); - if (!found) - found = lookForSearchEntryFrom(0); + config.elist_size_width = col_size_->GetWidth(); + saveStateInt("EntryListSizeWidth", config.elist_size_width); } - if (found) + // Type + if (col_type_ != last_col && col_type_->IsShown()) { - // Trigger selection change event - wxDataViewEvent de; - de.SetEventType(wxEVT_DATAVIEW_SELECTION_CHANGED); - de.SetString("search"); - ProcessWindowEvent(de); + config.elist_type_width = col_type_->GetWidth(); + saveStateInt("EntryListTypeWidth", config.elist_type_width); } - return true; + library::saveArchiveUIConfig(config); } -#endif // ----------------------------------------------------------------------------- // Sets the root directory to [dir] and updates UI accordingly diff --git a/src/UI/Lists/ArchiveEntryTree.h b/src/UI/Lists/ArchiveEntryTree.h index 122dfbaa4..6534ab325 100644 --- a/src/UI/Lists/ArchiveEntryTree.h +++ b/src/UI/Lists/ArchiveEntryTree.h @@ -1,7 +1,7 @@ #pragma once #include "General/Sigslot.h" -#include +#include "SDataViewCtrl.h" namespace slade { @@ -84,7 +84,7 @@ namespace ui bool dirIsInList(const ArchiveDir& dir) const; }; - class ArchiveEntryTree : public wxDataViewCtrl + class ArchiveEntryTree : public SDataViewCtrl { public: ArchiveEntryTree( @@ -102,6 +102,7 @@ namespace ui bool isSortedBySize() const { return GetSortingColumn() == col_size_; } bool isSortedByType() const { return GetSortingColumn() == col_type_; } bool isDefaultSorted() const; + bool isTreeView() const { return model_ ? model_->viewType() == ArchiveViewModel::ViewType::Tree : false; } vector selectedEntries(bool include_dirs = false) const; ArchiveEntry* firstSelectedEntry(bool include_dirs = false) const; @@ -132,22 +133,16 @@ namespace ui private: weak_ptr archive_; - ArchiveViewModel* model_ = nullptr; - wxDataViewColumn* col_name_ = nullptr; - wxDataViewColumn* col_size_ = nullptr; - wxDataViewColumn* col_type_ = nullptr; - wxDataViewColumn* col_index_ = nullptr; - int multi_select_base_index_ = -1; - string search_; + ArchiveViewModel* model_ = nullptr; + wxDataViewColumn* col_name_ = nullptr; + wxDataViewColumn* col_size_ = nullptr; + wxDataViewColumn* col_type_ = nullptr; + wxDataViewColumn* col_index_ = nullptr; void setupColumns(); - void saveColumnWidths() const; void updateColumnWidths(); - -#ifdef __WXMSW__ - bool lookForSearchEntryFrom(int index_start); - bool searchChar(int key_code); -#endif + void saveColumnConfig(); + void onAnyColumnResized() override; }; } // namespace ui diff --git a/src/UI/Lists/SDataViewCtrl.cpp b/src/UI/Lists/SDataViewCtrl.cpp new file mode 100644 index 000000000..ad3dec4ae --- /dev/null +++ b/src/UI/Lists/SDataViewCtrl.cpp @@ -0,0 +1,389 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: SDataViewCtrl.cpp +// Description: wxDataViewCtrl extension that handles some common extra +// functionality such as typing to search (in Windows), improved +// multiple selection (again in Windows), column visibility menus, +// etc. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "SDataViewCtrl.h" +#include "UI/State.h" +#include "UI/WxUtils.h" +#include "Utility/StringUtils.h" + +using namespace slade; +using namespace ui; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +wxDEFINE_EVENT(EVT_SDVC_COLUMN_RESIZED, wxDataViewEvent); + +vector real_chars = { + '.', ',', '_', '-', '+', '=', '`', '~', '!', '@', '#', '$', '(', ')', '[', + ']', '{', '}', ':', ';', '/', '\\', '<', '>', '?', '^', '&', '\'', '\"', +}; + + +// ----------------------------------------------------------------------------- +// +// SDataViewCtrl Class Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// SDataViewCtrl class constructor +// ----------------------------------------------------------------------------- +SDataViewCtrl::SDataViewCtrl(wxWindow* parent, long style) : wxDataViewCtrl(parent, -1, {}, {}, style) +{ + Bind( + wxEVT_IDLE, + [this](wxIdleEvent&) + { + auto max = GetColumnPosition(lastVisibleColumn()) - 1; // Ignore last column - is stretched + auto resized = false; + for (int i = 0; i <= max; ++i) + { + auto col = GetColumn(i); + auto index = col->GetModelColumn(); + + if (!col->IsHidden() && col->GetWidth() != column_widths_[index]) + { + column_widths_[index] = col->GetWidth(); + onColumnResized(col); + resized = true; + wxDataViewEvent de{ EVT_SDVC_COLUMN_RESIZED, this, col }; + ProcessWindowEvent(de); + } + } + + if (resized) + onAnyColumnResized(); + }); + +#ifdef __WXMSW__ + // Keypress event + Bind( + wxEVT_CHAR, + [this](wxKeyEvent& e) + { + // Custom handling for shift+up/down + if (e.ShiftDown()) + { + int from_row = multi_select_base_index_; + + // Get row to select to + // TODO: Handle PgUp/PgDn as well? + int to_row; + switch (e.GetKeyCode()) + { + case WXK_DOWN: to_row = GetRowByItem(GetCurrentItem()) + 1; break; + case WXK_UP: to_row = GetRowByItem(GetCurrentItem()) - 1; break; + default: + // Not up or down arrow, do default handling + e.Skip(); + return; + } + + // Get new item to focus + auto new_current_item = GetItemByRow(to_row); + if (!new_current_item.IsOk()) + { + e.Skip(); + return; + } + + // Ensure valid range + if (from_row > to_row) + std::swap(from_row, to_row); + + // Get items to select + wxDataViewItemArray items; + for (int i = from_row; i <= to_row; ++i) + items.Add(GetItemByRow(i)); + + // Set new selection + SetSelections(items); + SetCurrentItem(new_current_item); + + // Trigger selection change event + wxDataViewEvent de; + de.SetEventType(wxEVT_DATAVIEW_SELECTION_CHANGED); + ProcessWindowEvent(de); + + return; + } + + // Search + if (e.GetModifiers() == 0 && search_model_column_ >= 0) + { + if (searchChar(e.GetKeyCode())) + return; + } + + e.Skip(); + }); + + Bind( + wxEVT_DATAVIEW_SELECTION_CHANGED, + [this](wxDataViewEvent& e) + { + if (GetSelectedItemsCount() == 1) + multi_select_base_index_ = GetRowByItem(GetSelection()); + + // Clear search string if selection change wasn't a result of searching + if (e.GetString().Cmp("search")) + search_.clear(); + + e.Skip(); + }); + + Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { search_.clear(); }); +#endif +} + +// ----------------------------------------------------------------------------- +// Returns the last visible column +// ----------------------------------------------------------------------------- +wxDataViewColumn* SDataViewCtrl::lastVisibleColumn() const +{ + for (int i = static_cast(GetColumnCount()) - 1; i >= 0; --i) + if (!GetColumn(i)->IsHidden()) + return GetColumn(i); + + return nullptr; +} + +// ----------------------------------------------------------------------------- +// Resets sorting to the default state (ie. no columns selected for sorting) +// ----------------------------------------------------------------------------- +void SDataViewCtrl::resetSorting() +{ + for (unsigned c = 0; c < GetColumnCount(); ++c) + if (GetColumn(c)->IsSortKey()) + GetColumn(c)->UnsetAsSortKey(); +#ifdef __WXGTK__ + GetColumn(0)->SetSortOrder(true); +#else + if (GetColumn(0)->IsSortKey()) + GetColumn(0)->UnsetAsSortKey(); +#endif + GetModel()->Resort(); + + wxDataViewEvent de; + de.SetEventType(wxEVT_DATAVIEW_COLUMN_SORTED); + ProcessWindowEvent(de); +} + +// ----------------------------------------------------------------------------- +// Adds a check item to [menu] for showing/hiding a column [col_model] +// ----------------------------------------------------------------------------- +void SDataViewCtrl::appendColumnToggleItem(wxMenu& menu, int col_model) const +{ + for (unsigned i = 0; i < GetColumnCount(); ++i) + { + auto* col = GetColumn(i); + if (col->GetModelColumn() == col_model) + { + menu.AppendCheckItem(col_model, col->GetTitle(), wxString::Format("Show the %s column", col->GetTitle())) + ->Check(col->IsShown()); + + return; + } + } +} + +// ----------------------------------------------------------------------------- +// Toggles visibility of column [col_model], saving the result to UI state +// property [state_prop] +// ----------------------------------------------------------------------------- +void SDataViewCtrl::toggleColumnVisibility(int col_model, string_view state_prop) const +{ + auto* column = GetColumn(modelColumnIndex(col_model)); + + column->SetHidden(!column->IsHidden()); + + if (!state_prop.empty()) + saveStateBool(state_prop, column->IsShown()); +} + +// ----------------------------------------------------------------------------- +// Sets [column]'s [width] (column index in view) +// ----------------------------------------------------------------------------- +void SDataViewCtrl::setColumnWidth(wxDataViewColumn* column, int width) const +{ + if (!column || column->IsHidden()) + return; + + column->SetWidth(column == lastVisibleColumn() ? 0 : width); +} + +// ----------------------------------------------------------------------------- +// Sets [col_model]'s [width] (column index in model) +// ----------------------------------------------------------------------------- +void SDataViewCtrl::setColumnWidth(int col_model, int width) const +{ + setColumnWidth(GetColumn(modelColumnIndex(col_model)), width); +} + +// ----------------------------------------------------------------------------- +// Returns the index of the column for model column index [model_column] +// ----------------------------------------------------------------------------- +int SDataViewCtrl::modelColumnIndex(int model_column) const +{ + const int count = GetColumnCount(); + for (int index = 0; index < count; index++) + { + wxDataViewColumn* column = GetColumn(index); + if (column->GetModelColumn() == model_column) + return index; + } + return wxNOT_FOUND; +} + +#ifdef __WXMSW__ +// ----------------------------------------------------------------------------- +// Beginning from [index_start], finds and selects the first item which matches +// the internal search_ string on search_model_column_. +// Returns true if a match was found +// ----------------------------------------------------------------------------- +bool SDataViewCtrl::lookForSearchItemFrom(int index_start) +{ + long index = index_start; + wxVariant value; + wxString val_string; + wxDataViewIconText val_dvit; + + while (true) + { + auto item = GetItemByRow(index); + if (!item.IsOk()) + break; + + // Get column text + GetModel()->GetValue(value, item, search_model_column_); + if (value.GetType() == "string") + val_string = value.GetString(); + else if (value.GetType() == "wxDataViewIconText") + { + val_dvit << value; + val_string = val_dvit.GetText(); + } + + if (strutil::startsWithCI(wxutil::strToView(val_string), search_)) + { + // Matches, update selection+focus + wxDataViewItemArray items; + items.Add(item); + SetSelections(items); + SetCurrentItem(item); + EnsureVisible(item); + return true; + } + + ++index; + } + + // Didn't get any match + return false; +} + +// ----------------------------------------------------------------------------- +// Adds [key_code] to the current internal search string (if valid) and performs +// quick search. +// Returns false if the key was not a 'real' character usable for searching +// ----------------------------------------------------------------------------- +bool SDataViewCtrl::searchChar(int key_code) +{ + // Check the key pressed is actually a character (a-z, 0-9 etc) + bool real_char = false; + if (key_code >= 'a' && key_code <= 'z') // Lowercase + real_char = true; + else if (key_code >= 'A' && key_code <= 'Z') // Uppercase + real_char = true; + else if (key_code >= '0' && key_code <= '9') // Number + real_char = true; + else + { + for (int elist_char : real_chars) + { + if (key_code == elist_char) + { + real_char = true; + break; + } + } + } + + if (!real_char) + { + search_.clear(); + return false; + } + + // Get currently focused item (or first if nothing is focused) + auto index = GetRowByItem(GetCurrentItem()); + if (index < 0) + index = 0; + + // Build search string + search_ += static_cast(key_code); + + // Find matching item, beginning from current item + // If no match found, try again from the top + auto found = true; + if (!lookForSearchItemFrom(index)) + found = lookForSearchItemFrom(0); + + // No match, continue from next item with fresh search string + if (!found) + { + search_.clear(); + search_ += static_cast(key_code); + found = lookForSearchItemFrom(index + 1); + if (!found) + found = lookForSearchItemFrom(0); + } + + if (found) + { + // Trigger selection change event + wxDataViewEvent de; + de.SetEventType(wxEVT_DATAVIEW_SELECTION_CHANGED); + de.SetString("search"); + ProcessWindowEvent(de); + } + + return true; +} +#endif diff --git a/src/UI/Lists/SDataViewCtrl.h b/src/UI/Lists/SDataViewCtrl.h new file mode 100644 index 000000000..0838e5868 --- /dev/null +++ b/src/UI/Lists/SDataViewCtrl.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +// An event to indicate when a column has been resized +wxDECLARE_EVENT(EVT_SDVC_COLUMN_RESIZED, wxDataViewEvent); + +namespace slade::ui +{ +class SDataViewCtrl : public wxDataViewCtrl +{ +public: + SDataViewCtrl(wxWindow* parent, long style); + + wxDataViewColumn* lastVisibleColumn() const; + + void setSearchColumn(int col_model) { search_model_column_ = col_model; } + + void resetSorting(); + void appendColumnToggleItem(wxMenu& menu, int col_model) const; + void toggleColumnVisibility(int col_model, string_view state_prop) const; + void setColumnWidth(wxDataViewColumn* column, int width) const; + void setColumnWidth(int col_model, int width) const; + int modelColumnIndex(int model_column) const; + +protected: + mutable std::array column_widths_; // Doubt we'll ever need more than 50 columns + + virtual void onColumnResized(wxDataViewColumn* column) {} + virtual void onAnyColumnResized() {} + +private: + int multi_select_base_index_ = -1; + string search_; + int search_model_column_ = -1; + +#ifdef __WXMSW__ + bool lookForSearchItemFrom(int index_start); + bool searchChar(int key_code); +#endif +}; +} // namespace slade::ui diff --git a/src/UI/SDialog.cpp b/src/UI/SDialog.cpp index 941aa9d9a..7149860ac 100644 --- a/src/UI/SDialog.cpp +++ b/src/UI/SDialog.cpp @@ -32,7 +32,7 @@ // ----------------------------------------------------------------------------- #include "Main.h" #include "SDialog.h" -#include "General/Misc.h" +#include "General/UI.h" using namespace slade; @@ -52,14 +52,14 @@ SDialog::SDialog(wxWindow* parent, const wxString& title, const wxString& id, in id_{ id } { // Init size/pos - auto info = misc::getWindowInfo(id_); + auto info = ui::getWindowInfo(id_.c_str()); if (!info.id.empty()) { SetClientSize(info.width, info.height); SetPosition(wxPoint(info.left, info.top)); } else - misc::setWindowInfo(id_, width, height, x, y); + ui::setWindowInfo(id_.c_str(), width, height, x, y); // Bind events if (!id.empty()) @@ -76,8 +76,8 @@ SDialog::SDialog(wxWindow* parent, const wxString& title, const wxString& id, in SDialog::~SDialog() { if (!id_.empty()) - misc::setWindowInfo( - id_, + ui::setWindowInfo( + id_.c_str(), GetClientSize().x * GetContentScaleFactor(), GetClientSize().y * GetContentScaleFactor(), GetPosition().x * GetContentScaleFactor(), @@ -89,7 +89,7 @@ SDialog::~SDialog() // ----------------------------------------------------------------------------- void SDialog::setSavedSize(int def_width, int def_height) { - auto info = misc::getWindowInfo(id_); + auto info = ui::getWindowInfo(id_.c_str()); if (!info.id.empty()) { SetInitialSize(wxSize(info.width, info.height)); @@ -119,7 +119,7 @@ void SDialog::onSize(wxSizeEvent& e) { // Update window size settings const wxSize ClientSize = GetClientSize() * GetContentScaleFactor(); - misc::setWindowInfo(id_, ClientSize.x, ClientSize.y, -2, -2); + ui::setWindowInfo(id_.c_str(), ClientSize.x, ClientSize.y, -2, -2); e.Skip(); } @@ -129,7 +129,7 @@ void SDialog::onSize(wxSizeEvent& e) void SDialog::onMove(wxMoveEvent& e) { // Update window position settings - misc::setWindowInfo(id_, -2, -2, GetPosition().x, GetPosition().y); + ui::setWindowInfo(id_.c_str(), GetClientSize().x, GetClientSize().y, GetPosition().x, GetPosition().y); e.Skip(); } diff --git a/src/UI/STopWindow.cpp b/src/UI/STopWindow.cpp index 5f1346877..bf7b255f2 100644 --- a/src/UI/STopWindow.cpp +++ b/src/UI/STopWindow.cpp @@ -32,8 +32,8 @@ // ----------------------------------------------------------------------------- #include "Main.h" #include "STopWindow.h" -#include "General/Misc.h" #include "General/SAction.h" +#include "General/UI.h" #include "SToolBar/SToolBar.h" #include "Utility/StringUtils.h" @@ -67,14 +67,14 @@ STopWindow::STopWindow(const wxString& title, const wxString& id, int x, int y, #ifndef __WXOSX__ // Init size/pos - auto info = misc::getWindowInfo(id_); + auto info = ui::getWindowInfo(id_.c_str()); if (!info.id.empty()) { SetSize(info.width, info.height); SetPosition(wxPoint(info.left, info.top)); } else - misc::setWindowInfo(id_, width, height, x, y); + ui::setWindowInfo(id_.c_str(), width, height, x, y); #endif // Init toolbar menu action(s) @@ -93,7 +93,7 @@ STopWindow::STopWindow(const wxString& title, const wxString& id, int x, int y, STopWindow::~STopWindow() { if (!wxFrame::IsMaximized() && !wxFrame::IsFullScreen()) - misc::setWindowInfo(id_, GetSize().x, GetSize().y, GetPosition().x, GetPosition().y); + ui::setWindowInfo(id_.c_str(), GetSize().x, GetSize().y, GetPosition().x, GetPosition().y); } // ----------------------------------------------------------------------------- diff --git a/src/UI/State.cpp b/src/UI/State.cpp new file mode 100644 index 000000000..1050557a4 --- /dev/null +++ b/src/UI/State.cpp @@ -0,0 +1,279 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: State.cpp +// Description: Functions handling database storage/retrieval of UI state info +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "State.h" +#include "Database/Database.h" +#include "Database/DbUtils.h" +#include "Database/Statement.h" +#include "General/UI.h" +#include "Utility/Named.h" +#include "Utility/Property.h" +#include + +using namespace slade; +using namespace ui; + + +// ----------------------------------------------------------------------------- +// +// Variables +// +// ----------------------------------------------------------------------------- +namespace +{ +string get_ui_state = "SELECT value FROM ui_state WHERE name = ?"; +string put_ui_state = "INSERT OR REPLACE INTO ui_state (name, value) VALUES (?,?)"; +} // namespace + + +// ----------------------------------------------------------------------------- +// +// UI Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Initializes UI state values to defaults in the database +// (if they don't already exist there) +// ----------------------------------------------------------------------------- +void ui::initStateProps() +{ + // Set default values + vector> props = { { "EntryListIndexVisible", false }, + { "EntryListIndexWidth", scalePx(50) }, + { "EntryListSizeVisible", true }, + { "EntryListSizeWidth", scalePx(80) }, + { "EntryListTypeVisible", true }, + { "EntryListTypeWidth", scalePx(150) }, + { "EntryListNameWidthList", scalePx(150) }, + { "EntryListNameWidthTree", scalePx(180) }, + { "ArchivePanelSplitPosList", scalePx(300) }, + { "ArchivePanelSplitPosTree", scalePx(300) }, + { "ArchiveLastCreatedFormat", "wad" }, + { "ColouriseDialogLastColour", "RGB(255, 0, 0)" }, + { "TintDialogLastColour", "RGB(255, 0, 0)" }, + { "TintDialogLastAmount", 50 }, + { "ZoomGfxCanvas", 100 }, + { "ZoomCTextureCanvas", 100 }, + { "LibraryPanelFilenameWidth", scalePx(250) }, + { "LibraryPanelPathVisible", true }, + { "LibraryPanelPathWidth", scalePx(250) }, + { "LibraryPanelSizeVisible", true }, + { "LibraryPanelSizeWidth", scalePx(100) }, + { "LibraryPanelTypeVisible", true }, + { "LibraryPanelTypeWidth", scalePx(150) }, + { "LibraryPanelLastOpenedVisible", true }, + { "LibraryPanelLastOpenedWidth", scalePx(200) }, + { "LibraryPanelFileModifiedVisible", true }, + { "LibraryPanelFileModifiedWidth", scalePx(200) }, + { "LibraryPanelEntryCountVisible", true }, + { "LibraryPanelEntryCountWidth", scalePx(80) }, + { "LibraryPanelMapCountVisible", true }, + { "LibraryPanelMapCountWidth", scalePx(80) }, + { "BrowserWindowMaximized", false }, + { "MainWindowMaximized", true }, + { "MapEditorWindowMaximized", true }, + { "ScriptManagerWindowMaximized", false }, + { "SetupWizardRun", false } }; + + if (auto sql = db::cacheQuery("init_ui_state", "INSERT OR IGNORE INTO ui_state VALUES (?,?)", true)) + { + for (const auto& prop : props) + { + sql->bind(1, prop.name); + switch (prop.value.index()) + { + case 0: sql->bind(2, std::get(prop.value)); break; + case 1: sql->bind(2, std::get(prop.value)); break; + case 2: sql->bind(2, std::get(prop.value)); break; + case 3: sql->bind(2, std::get(prop.value)); break; + case 4: sql->bind(2, std::get(prop.value)); break; + default: sql->clearBindings(); continue; + } + + sql->exec(); + sql->reset(); + } + } +} + +// ----------------------------------------------------------------------------- +// Returns true if saved state [name] exists in the database +// ----------------------------------------------------------------------------- +bool ui::hasSavedState(string_view name) +{ + return db::rowExists(*db::connectionRO(), "ui_state", "name", name); +} + +// ----------------------------------------------------------------------------- +// Returns boolean UI state value [name] +// ----------------------------------------------------------------------------- +bool ui::getStateBool(string_view name) +{ + auto val = false; + + if (auto sql = db::cacheQuery("get_ui_state", get_ui_state)) + { + sql->bind(1, name); + if (sql->executeStep()) + val = sql->getColumn(0).getInt() > 0; + sql->reset(); + } + + return val; +} + +// ----------------------------------------------------------------------------- +// Returns int UI state value [name] +// ----------------------------------------------------------------------------- +int ui::getStateInt(string_view name) +{ + auto val = 0; + + if (auto sql = db::cacheQuery("get_ui_state", get_ui_state)) + { + sql->bind(1, name); + if (sql->executeStep()) + val = sql->getColumn(0).getInt(); + sql->reset(); + } + + return val; +} + +// ----------------------------------------------------------------------------- +// Returns float UI state value [name] +// ----------------------------------------------------------------------------- +double ui::getStateFloat(string_view name) +{ + auto val = 0.; + + if (auto sql = db::cacheQuery("get_ui_state", get_ui_state)) + { + sql->bind(1, name); + if (sql->executeStep()) + val = sql->getColumn(0).getDouble(); + sql->reset(); + } + + return val; +} + +// ----------------------------------------------------------------------------- +// Returns string UI state value [name] +// ----------------------------------------------------------------------------- +string ui::getStateString(string_view name) +{ + string val; + + if (auto sql = db::cacheQuery("get_ui_state", get_ui_state)) + { + sql->bind(1, name); + if (sql->executeStep()) + val = sql->getColumn(0).getString(); + sql->reset(); + } + + return val; +} + +// ----------------------------------------------------------------------------- +// Sets UI boolean state [name] to [value] in the database +// ----------------------------------------------------------------------------- +void ui::saveStateBool(string_view name, bool value) +{ + if (auto sql = db::cacheQuery("put_ui_state", put_ui_state, true)) + { + sql->bind(1, name); + sql->bind(2, value); + sql->exec(); + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Sets UI int state [name] to [value] in the database +// ----------------------------------------------------------------------------- +void ui::saveStateInt(string_view name, int value) +{ + if (auto sql = db::cacheQuery("put_ui_state", put_ui_state, true)) + { + sql->bind(1, name); + sql->bind(2, value); + sql->exec(); + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Sets UI float state [name] to [value] in the database +// ----------------------------------------------------------------------------- +void ui::saveStateFloat(string_view name, double value) +{ + if (auto sql = db::cacheQuery("put_ui_state", put_ui_state, true)) + { + sql->bind(1, name); + sql->bind(2, value); + sql->exec(); + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Sets UI string state [name] to [value] in the database +// ----------------------------------------------------------------------------- +void ui::saveStateString(string_view name, string_view value) +{ + if (auto sql = db::cacheQuery("put_ui_state", put_ui_state, true)) + { + sql->bind(1, name); + sql->bind(2, value); + sql->exec(); + sql->reset(); + } +} + +// ----------------------------------------------------------------------------- +// Toggles UI boolean state [name] +// ----------------------------------------------------------------------------- +void ui::toggleStateBool(string_view name) +{ + if (auto sql = db::cacheQuery( + "toggle_ui_state_bool", + "UPDATE ui_state SET value = CASE value WHEN 0 THEN 1 ELSE 0 END WHERE name = ?", + true)) + { + sql->bind(1, name); + sql->exec(); + sql->reset(); + } +} diff --git a/src/UI/State.h b/src/UI/State.h new file mode 100644 index 000000000..c01fdfa69 --- /dev/null +++ b/src/UI/State.h @@ -0,0 +1,18 @@ +#pragma once + +namespace slade::ui +{ +void initStateProps(); +bool hasSavedState(string_view name); + +bool getStateBool(string_view name); +int getStateInt(string_view name); +double getStateFloat(string_view name); +string getStateString(string_view name); + +void saveStateBool(string_view name, bool value); +void saveStateInt(string_view name, int value); +void saveStateFloat(string_view name, double value); +void saveStateString(string_view name, string_view value); +void toggleStateBool(string_view name); +} // namespace slade::ui diff --git a/src/UI/WxUtils.cpp b/src/UI/WxUtils.cpp index a4b8e355f..1fce78db9 100644 --- a/src/UI/WxUtils.cpp +++ b/src/UI/WxUtils.cpp @@ -132,9 +132,9 @@ wxPanel* wxutil::createPadPanel(wxWindow* parent, wxWindow* control, int pad) pad = ui::pad(); auto panel = new wxPanel(parent); + control->Reparent(panel); panel->SetSizer(new wxBoxSizer(wxVERTICAL)); panel->GetSizer()->Add(control, 1, wxEXPAND | wxALL, pad); - control->Reparent(panel); return panel; } diff --git a/src/Utility/DateTime.cpp b/src/Utility/DateTime.cpp new file mode 100644 index 000000000..c774b4bfb --- /dev/null +++ b/src/Utility/DateTime.cpp @@ -0,0 +1,88 @@ + +// ----------------------------------------------------------------------------- +// SLADE - It's a Doom Editor +// Copyright(C) 2008 - 2024 Simon Judd +// +// Email: sirjuddington@gmail.com +// Web: http://slade.mancubus.net +// Filename: DateTime.cpp +// Description: Date/Time utility functions +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// +// Includes +// +// ----------------------------------------------------------------------------- +#include "Main.h" +#include "DateTime.h" +#include "thirdparty/fmt/include/fmt/chrono.h" +#include +#include + +using namespace slade; +using namespace datetime; + + +// ----------------------------------------------------------------------------- +// +// DateTime Namespace Functions +// +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Returns the current time (local) +// ----------------------------------------------------------------------------- +time_t datetime::now() +{ + using clock = std::chrono::system_clock; + return clock::to_time_t(clock::now()); +} + +// ----------------------------------------------------------------------------- +// Returns [time_utc] in the local timezone +// ----------------------------------------------------------------------------- +time_t datetime::toLocalTime(time_t time_utc) +{ + return mktime(std::localtime(&time_utc)); +} + +// ----------------------------------------------------------------------------- +// Returns [time_local] in the UTC timezone +// ----------------------------------------------------------------------------- +time_t datetime::toUniversalTime(time_t time_local) +{ + return mktime(std::gmtime(&time_local)); +} + +// ----------------------------------------------------------------------------- +// Returns [time] as a formatted string +// ----------------------------------------------------------------------------- +string datetime::toString(time_t time, Format format, string_view custom_format) +{ + auto as_tm = *std::localtime(&time); + + switch (format) + { + case Format::ISO: return fmt::format("{:%F %T}", as_tm); + case Format::Local: return fmt::format("{:%c}", as_tm); + case Format::Custom: return fmt::format(fmt::format("{{:{}}}", custom_format), as_tm); + } + + return {}; +} diff --git a/src/Utility/DateTime.h b/src/Utility/DateTime.h new file mode 100644 index 000000000..fb1578619 --- /dev/null +++ b/src/Utility/DateTime.h @@ -0,0 +1,16 @@ +#pragma once + +namespace slade::datetime +{ +enum class Format +{ + ISO, + Local, + Custom +}; + +time_t now(); +time_t toLocalTime(time_t time_utc); +time_t toUniversalTime(time_t time_local); +string toString(time_t time, Format format = Format::ISO, string_view custom_format = {}); +} // namespace slade::datetime diff --git a/src/Utility/FileMonitor.cpp b/src/Utility/FileMonitor.cpp index 102ada4fc..b48ed1d2c 100644 --- a/src/Utility/FileMonitor.cpp +++ b/src/Utility/FileMonitor.cpp @@ -137,7 +137,7 @@ void DB2MapFileMonitor::fileModified() // Load file into temp archive auto wad = std::make_unique(ArchiveFormat::Wad); - wad->open(filename_); + wad->open(filename_, true); // Get map info for target archive for (auto& map : archive_->detectMaps()) diff --git a/src/Utility/FileUtils.cpp b/src/Utility/FileUtils.cpp index 3f1bee570..afeb97f4b 100644 --- a/src/Utility/FileUtils.cpp +++ b/src/Utility/FileUtils.cpp @@ -1,7 +1,7 @@ // ----------------------------------------------------------------------------- // SLADE - It's a Doom Editor -// Copyright(C) 2008 - 2017 Simon Judd +// Copyright(C) 2008 - 2024 Simon Judd // // Email: sirjuddington@gmail.com // Web: http://slade.mancubus.net @@ -34,7 +34,9 @@ #include "Main.h" #include "FileUtils.h" #include "App.h" +#include "MD5.h" #include "StringUtils.h" +#include "thirdparty/xxhash/xxhash.h" #include #include @@ -261,6 +263,11 @@ time_t fileutil::fileModifiedTime(string_view path) return wxFileModificationTime(wxString{ path.data(), path.length() }); } +string fileutil::fileHash(string_view path) +{ + SFile file(path); + return file.calculateHash(); +} // ----------------------------------------------------------------------------- @@ -403,3 +410,71 @@ bool SFile::writeStr(string_view str) const return false; } + +// ----------------------------------------------------------------------------- +// Calculates the MD5 hash of the file and returns it as a string +// ----------------------------------------------------------------------------- +string SFile::calculateMD5() const +{ + MD5 md5; + auto current_pos = currentPos(); + auto size = this->size(); + + seekFromStart(0); + unsigned pos = 0; + md5.init(); + + // Read in 1mb chunks + unsigned chunk_size = 1024; + char buffer[1024]; + while (pos < size) + { + if (size - pos < chunk_size) + chunk_size = size - pos; + + read(buffer, chunk_size); + md5.update(buffer, chunk_size); + + pos += chunk_size; + } + + md5.finalize(); + seekFromStart(current_pos); + + return md5.hexdigest(); +} + +string SFile::calculateHash() const +{ + if (!isOpen()) + return {}; + + auto current_pos = currentPos(); + auto size = this->size(); + + seekFromStart(0); + unsigned pos = 0; + + auto* state = XXH3_createState(); + XXH3_128bits_reset(state); + + // Read in 1mb chunks + unsigned chunk_size = 1024; + char buffer[1024]; + while (pos < size) + { + if (size - pos < chunk_size) + chunk_size = size - pos; + + read(buffer, chunk_size); + XXH3_128bits_update(state, buffer, chunk_size); + + pos += chunk_size; + } + + auto hash = XXH3_128bits_digest(state); + XXH3_freeState(state); + seekFromStart(current_pos); + + return fmt::format("{:x}{:x}", hash.high64, hash.low64); +} diff --git a/src/Utility/FileUtils.h b/src/Utility/FileUtils.h index 379099d01..be917cb8e 100644 --- a/src/Utility/FileUtils.h +++ b/src/Utility/FileUtils.h @@ -17,6 +17,7 @@ namespace fileutil bool removeDir(string_view path); vector allFilesInDir(string_view path, bool include_subdirs = false, bool include_dir_paths = false); time_t fileModifiedTime(string_view path); + string fileHash(string_view path); } // namespace fileutil class SFile : public SeekableData @@ -53,6 +54,9 @@ class SFile : public SeekableData bool write(const void* buffer, unsigned count) override; bool writeStr(string_view str) const; + string calculateMD5() const; + string calculateHash() const; + private: FILE* handle_ = nullptr; struct stat stat_; diff --git a/src/Utility/MD5.cpp b/src/Utility/MD5.cpp new file mode 100644 index 000000000..41f6f3508 --- /dev/null +++ b/src/Utility/MD5.cpp @@ -0,0 +1,388 @@ + +/* MD5 +converted to C++ class by Frank Thilo (thilo@unix-ag.org) +for bzflag (http://www.bzflag.org) + +based on: + +md5.h and md5.c +reference implemantion of RFC 1321 + +Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + +*/ + +/* interface header */ + +#include "Main.h" +#include "MD5.h" + +/* system implementation headers */ +#include + + +// Constants for MD5Transform routine. +#define S11 7 +#define S12 12 +#define S13 17 +#define S14 22 +#define S21 5 +#define S22 9 +#define S23 14 +#define S24 20 +#define S31 4 +#define S32 11 +#define S33 16 +#define S34 23 +#define S41 6 +#define S42 10 +#define S43 15 +#define S44 21 + +/////////////////////////////////////////////// + +// F, G, H and I are basic MD5 functions. +inline MD5::uint4 MD5::F(uint4 x, uint4 y, uint4 z) +{ + return x & y | ~x & z; +} + +inline MD5::uint4 MD5::G(uint4 x, uint4 y, uint4 z) +{ + return x & z | y & ~z; +} + +inline MD5::uint4 MD5::H(uint4 x, uint4 y, uint4 z) +{ + return x ^ y ^ z; +} + +inline MD5::uint4 MD5::I(uint4 x, uint4 y, uint4 z) +{ + return y ^ (x | ~z); +} + +// rotate_left rotates x left n bits. +inline MD5::uint4 MD5::rotate_left(uint4 x, int n) +{ + return (x << n) | (x >> (32 - n)); +} + +// FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4. +// Rotation is separate from addition to prevent recomputation. +inline void MD5::FF(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) +{ + a = rotate_left(a + F(b, c, d) + x + ac, s) + b; +} + +inline void MD5::GG(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) +{ + a = rotate_left(a + G(b, c, d) + x + ac, s) + b; +} + +inline void MD5::HH(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) +{ + a = rotate_left(a + H(b, c, d) + x + ac, s) + b; +} + +inline void MD5::II(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) +{ + a = rotate_left(a + I(b, c, d) + x + ac, s) + b; +} + +////////////////////////////////////////////// + +// default ctor, just initailize +MD5::MD5() +{ + init(); +} + +////////////////////////////////////////////// + +// nifty shortcut ctor, compute MD5 for string and finalize it right away +MD5::MD5(const std::string& text) +{ + init(); + update(text.c_str(), text.length()); + finalize(); +} + +MD5::MD5(const char* data, size_type length) +{ + init(); + update(data, length); + finalize(); +} + +MD5::MD5(const slade::MemChunk& data) +{ + init(); + update(data.data(), data.size()); + finalize(); +} + +////////////////////////////// + +void MD5::init() +{ + finalized = false; + + count[0] = 0; + count[1] = 0; + + // load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; +} + +////////////////////////////// + +// decodes input (unsigned char) into output (uint4). Assumes len is a multiple of 4. +void MD5::decode(uint4 output[], const uint1 input[], size_type len) +{ + for (unsigned int i = 0, j = 0; j < len; i++, j += 4) + output[i] = ((uint4)input[j]) | (((uint4)input[j + 1]) << 8) | (((uint4)input[j + 2]) << 16) + | (((uint4)input[j + 3]) << 24); +} + +////////////////////////////// + +// encodes input (uint4) into output (unsigned char). Assumes len is +// a multiple of 4. +void MD5::encode(uint1 output[], const uint4 input[], size_type len) +{ + for (size_type i = 0, j = 0; j < len; i++, j += 4) + { + output[j] = input[i] & 0xff; + output[j + 1] = (input[i] >> 8) & 0xff; + output[j + 2] = (input[i] >> 16) & 0xff; + output[j + 3] = (input[i] >> 24) & 0xff; + } +} + +////////////////////////////// + +// apply MD5 algo on a block +void MD5::transform(const uint1 block[blocksize]) +{ + uint4 a = state[0], b = state[1], c = state[2], d = state[3], x[16]; + decode(x, block, blocksize); + + /* Round 1 */ + FF(a, b, c, d, x[0], S11, 0xd76aa478); /* 1 */ + FF(d, a, b, c, x[1], S12, 0xe8c7b756); /* 2 */ + FF(c, d, a, b, x[2], S13, 0x242070db); /* 3 */ + FF(b, c, d, a, x[3], S14, 0xc1bdceee); /* 4 */ + FF(a, b, c, d, x[4], S11, 0xf57c0faf); /* 5 */ + FF(d, a, b, c, x[5], S12, 0x4787c62a); /* 6 */ + FF(c, d, a, b, x[6], S13, 0xa8304613); /* 7 */ + FF(b, c, d, a, x[7], S14, 0xfd469501); /* 8 */ + FF(a, b, c, d, x[8], S11, 0x698098d8); /* 9 */ + FF(d, a, b, c, x[9], S12, 0x8b44f7af); /* 10 */ + FF(c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ + FF(b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ + FF(a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ + FF(d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ + FF(c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ + FF(b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ + + /* Round 2 */ + GG(a, b, c, d, x[1], S21, 0xf61e2562); /* 17 */ + GG(d, a, b, c, x[6], S22, 0xc040b340); /* 18 */ + GG(c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ + GG(b, c, d, a, x[0], S24, 0xe9b6c7aa); /* 20 */ + GG(a, b, c, d, x[5], S21, 0xd62f105d); /* 21 */ + GG(d, a, b, c, x[10], S22, 0x2441453); /* 22 */ + GG(c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ + GG(b, c, d, a, x[4], S24, 0xe7d3fbc8); /* 24 */ + GG(a, b, c, d, x[9], S21, 0x21e1cde6); /* 25 */ + GG(d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ + GG(c, d, a, b, x[3], S23, 0xf4d50d87); /* 27 */ + GG(b, c, d, a, x[8], S24, 0x455a14ed); /* 28 */ + GG(a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ + GG(d, a, b, c, x[2], S22, 0xfcefa3f8); /* 30 */ + GG(c, d, a, b, x[7], S23, 0x676f02d9); /* 31 */ + GG(b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ + HH(a, b, c, d, x[5], S31, 0xfffa3942); /* 33 */ + HH(d, a, b, c, x[8], S32, 0x8771f681); /* 34 */ + HH(c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ + HH(b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ + HH(a, b, c, d, x[1], S31, 0xa4beea44); /* 37 */ + HH(d, a, b, c, x[4], S32, 0x4bdecfa9); /* 38 */ + HH(c, d, a, b, x[7], S33, 0xf6bb4b60); /* 39 */ + HH(b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ + HH(a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ + HH(d, a, b, c, x[0], S32, 0xeaa127fa); /* 42 */ + HH(c, d, a, b, x[3], S33, 0xd4ef3085); /* 43 */ + HH(b, c, d, a, x[6], S34, 0x4881d05); /* 44 */ + HH(a, b, c, d, x[9], S31, 0xd9d4d039); /* 45 */ + HH(d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ + HH(c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ + HH(b, c, d, a, x[2], S34, 0xc4ac5665); /* 48 */ + + /* Round 4 */ + II(a, b, c, d, x[0], S41, 0xf4292244); /* 49 */ + II(d, a, b, c, x[7], S42, 0x432aff97); /* 50 */ + II(c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ + II(b, c, d, a, x[5], S44, 0xfc93a039); /* 52 */ + II(a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ + II(d, a, b, c, x[3], S42, 0x8f0ccc92); /* 54 */ + II(c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ + II(b, c, d, a, x[1], S44, 0x85845dd1); /* 56 */ + II(a, b, c, d, x[8], S41, 0x6fa87e4f); /* 57 */ + II(d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ + II(c, d, a, b, x[6], S43, 0xa3014314); /* 59 */ + II(b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ + II(a, b, c, d, x[4], S41, 0xf7537e82); /* 61 */ + II(d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ + II(c, d, a, b, x[2], S43, 0x2ad7d2bb); /* 63 */ + II(b, c, d, a, x[9], S44, 0xeb86d391); /* 64 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + + // Zeroize sensitive information. + memset(x, 0, sizeof x); +} + +////////////////////////////// + +// MD5 block update operation. Continues an MD5 message-digest +// operation, processing another message block +void MD5::update(const unsigned char input[], size_type length) +{ + // compute number of bytes mod 64 + size_type index = count[0] / 8 % blocksize; + + // Update number of bits + if ((count[0] += (length << 3)) < (length << 3)) + count[1]++; + count[1] += (length >> 29); + + // number of bytes we need to fill in buffer + size_type firstpart = 64 - index; + + size_type i; + + // transform as many times as possible. + if (length >= firstpart) + { + // fill buffer first, transform + memcpy(&buffer[index], input, firstpart); + transform(buffer); + + // transform chunks of blocksize (64 bytes) + for (i = firstpart; i + blocksize <= length; i += blocksize) + transform(&input[i]); + + index = 0; + } + else + i = 0; + + // buffer remaining input + memcpy(&buffer[index], &input[i], length - i); +} + +////////////////////////////// + +// for convenience provide a verson with signed char +void MD5::update(const char input[], size_type length) +{ + update((const unsigned char*)input, length); +} + +////////////////////////////// + +// MD5 finalization. Ends an MD5 message-digest operation, writing the +// the message digest and zeroizing the context. +MD5& MD5::finalize() +{ + static unsigned char padding[64] = { 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + if (!finalized) + { + // Save number of bits + unsigned char bits[8]; + encode(bits, count, 8); + + // pad out to 56 mod 64. + size_type index = count[0] / 8 % 64; + size_type padLen = (index < 56) ? (56 - index) : (120 - index); + update(padding, padLen); + + // Append length (before padding) + update(bits, 8); + + // Store state in digest + encode(digest, state, 16); + + // Zeroize sensitive information. + memset(buffer, 0, sizeof buffer); + memset(count, 0, sizeof count); + + finalized = true; + } + + return *this; +} + +////////////////////////////// + +// return hex representation of digest as string +std::string MD5::hexdigest() const +{ + if (!finalized) + return ""; + + char buf[33]; + for (int i = 0; i < 16; i++) + sprintf(buf + i * 2, "%02x", digest[i]); + buf[32] = 0; + + return std::string(buf); +} + +////////////////////////////// + +// std::ostream& operator<<(std::ostream& out, MD5 md5) +//{ +// return out << md5.hexdigest(); +//} + +////////////////////////////// + +// std::string md5(const std::string str) +//{ +// MD5 md5 = MD5(str); +// +// return md5.hexdigest(); +//} diff --git a/src/Utility/MD5.h b/src/Utility/MD5.h new file mode 100644 index 000000000..58fc0eb3d --- /dev/null +++ b/src/Utility/MD5.h @@ -0,0 +1,103 @@ + +/* MD5 +converted to C++ class by Frank Thilo (thilo@unix-ag.org) +for bzflag (http://www.bzflag.org) + +based on: + +md5.h and md5.c +reference implementation of RFC 1321 + +Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + +*/ + +#ifndef BZF_MD5_H +#define BZF_MD5_H + +#include +#include + +namespace slade +{ +class MemChunk; +} + +// a small class for calculating MD5 hashes of strings or byte arrays +// it is not meant to be fast or secure +// +// usage: 1) feed it blocks of uchars with update() +// 2) finalize() +// 3) get hexdigest() string +// or +// MD5(std::string).hexdigest() +// +// assumes that char is 8 bit and int is 32 bit +class MD5 +{ +public: + typedef unsigned int size_type; // must be 32bit + + MD5(); + MD5(const std::string& text); + MD5(const char* data, size_type length); + MD5(const slade::MemChunk& data); + void init(); + void update(const unsigned char* buf, size_type length); + void update(const char* buf, size_type length); + MD5& finalize(); + std::string hexdigest() const; + friend std::ostream& operator<<(std::ostream&, MD5 md5); + +private: + typedef unsigned char uint1; // 8bit + typedef unsigned int uint4; // 32bit + enum + { + blocksize = 64 + }; // VC6 won't eat a const static int here + + void transform(const uint1 block[blocksize]); + static void decode(uint4 output[], const uint1 input[], size_type len); + static void encode(uint1 output[], const uint4 input[], size_type len); + + bool finalized; + uint1 buffer[blocksize]; // bytes that didn't fit in last 64 byte chunk + uint4 count[2]; // 64bit counter for number of bits (lo, hi) + uint4 state[4]; // digest so far + uint1 digest[16]; // the result + + // low level logic operations + static inline uint4 F(uint4 x, uint4 y, uint4 z); + static inline uint4 G(uint4 x, uint4 y, uint4 z); + static inline uint4 H(uint4 x, uint4 y, uint4 z); + static inline uint4 I(uint4 x, uint4 y, uint4 z); + static inline uint4 rotate_left(uint4 x, int n); + static inline void FF(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void GG(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void HH(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void II(uint4& a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); +}; + +std::string md5(const std::string str); + +#endif diff --git a/src/Utility/MemChunk.cpp b/src/Utility/MemChunk.cpp index 6fbcf37d9..c27fa3faa 100644 --- a/src/Utility/MemChunk.cpp +++ b/src/Utility/MemChunk.cpp @@ -34,6 +34,8 @@ #include "MemChunk.h" #include "FileUtils.h" #include "General/Misc.h" +#include "MD5.h" +#include "thirdparty/xxhash/xxhash.h" using namespace slade; @@ -582,6 +584,32 @@ string MemChunk::asString(uint32_t offset, uint32_t length) const return s; } +// ----------------------------------------------------------------------------- +// Calculates the MD5 hash of the data. +// Returns the MD5 hash or an empty string if no data is present +// ----------------------------------------------------------------------------- +string MemChunk::md5() const +{ + if (!hasData()) + return {}; + + MD5 md5(*this); + return md5.hexdigest(); +} + +// ----------------------------------------------------------------------------- +// Calculates a 128-bit hash of the data using xxHash (XXH128). +// Returns the hash as a hex string or empty if no data is present +// ----------------------------------------------------------------------------- +string MemChunk::hash() const +{ + if (!hasData()) + return {}; + + auto hash = XXH3_128bits(data_, size_); + return fmt::format("{:x}{:x}", hash.high64, hash.low64); +} + // ----------------------------------------------------------------------------- // 'Releases' the MemChunk's data, returning a pointer to it and resetting the // MemChunk itself. diff --git a/src/Utility/MemChunk.h b/src/Utility/MemChunk.h index 33ae8921c..fc8249230 100644 --- a/src/Utility/MemChunk.h +++ b/src/Utility/MemChunk.h @@ -62,6 +62,8 @@ class MemChunk : public SeekableData bool fillData(uint8_t val) const; uint32_t crc() const; string asString(uint32_t offset = 0, uint32_t length = 0) const; + string md5() const; + string hash() const; uint8_t* releaseData(); // Platform-independent functions to read values in little (L##) or big (B##) endian diff --git a/src/Utility/SFileDialog.cpp b/src/Utility/SFileDialog.cpp index 0d6c87b32..117469a0a 100644 --- a/src/Utility/SFileDialog.cpp +++ b/src/Utility/SFileDialog.cpp @@ -35,19 +35,12 @@ #include "SFileDialog.h" #include "App.h" #include "StringUtils.h" +#include "UI/State.h" #include "UI/WxUtils.h" using namespace slade; -// ----------------------------------------------------------------------------- -// -// External Variables -// -// ----------------------------------------------------------------------------- -EXTERN_CVAR(String, dir_last) - - // ----------------------------------------------------------------------------- // // SFileDialog Namespace Functions @@ -71,7 +64,7 @@ bool filedialog::openFile( wxFileDialog fd( parent, wxutil::strFromView(caption), - dir_last, + ui::getStateString("FileDialogLastDir"), wxutil::strFromView(fn_default), wxutil::strFromView(extensions), wxFD_OPEN | wxFD_FILE_MUST_EXIST); @@ -90,7 +83,7 @@ bool filedialog::openFile( info.path = fn.path(true); // Set last dir - dir_last = info.path; + ui::saveStateString("FileDialogLastDir", info.path); return true; } @@ -114,7 +107,7 @@ string filedialog::openFile( wxFileDialog fd( parent, wxutil::strFromView(caption), - dir_last, + ui::getStateString("FileDialogLastDir"), wxutil::strFromView(fn_default), wxutil::strFromView(extensions), wxFD_OPEN | wxFD_FILE_MUST_EXIST); @@ -126,7 +119,7 @@ string filedialog::openFile( if (fd.ShowModal() == wxID_OK) { auto filename = fd.GetPath().ToStdString(); - dir_last = strutil::Path::pathOf(filename); + ui::saveStateString("FileDialogLastDir", string{ strutil::Path::pathOf(filename) }); return filename; } @@ -175,7 +168,7 @@ bool filedialog::openFiles( wxFileDialog fd( parent, wxutil::strFromView(caption), - dir_last, + ui::getStateString("FileDialogLastDir"), wxutil::strFromView(fn_default), wxutil::strFromView(extensions), wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE); @@ -198,7 +191,7 @@ bool filedialog::openFiles( info.path = fn.path(true); // Set last dir - dir_last = info.path; + ui::saveStateString("FileDialogLastDir", info.path); return true; } @@ -239,7 +232,7 @@ bool filedialog::saveFile( wxFileDialog fd( parent, wxutil::strFromView(caption), - dir_last, + ui::getStateString("FileDialogLastDir"), wxutil::strFromView(fn_default), wxutil::strFromView(extensions), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); @@ -258,7 +251,7 @@ bool filedialog::saveFile( info.path = fn.path(true); // Set last dir - dir_last = info.path; + ui::saveStateString("FileDialogLastDir", info.path); return true; } @@ -282,7 +275,7 @@ string filedialog::saveFile( wxFileDialog fd( parent, wxutil::strFromView(caption), - dir_last, + ui::getStateString("FileDialogLastDir"), wxutil::strFromView(fn_default), wxutil::strFromView(extensions), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); @@ -294,7 +287,7 @@ string filedialog::saveFile( if (fd.ShowModal() == wxID_OK) { auto filename = fd.GetPath().ToStdString(); - dir_last = strutil::Path::pathOf(filename); + ui::saveStateString("FileDialogLastDir", string{ strutil::Path::pathOf(filename) }); return filename; } @@ -312,7 +305,7 @@ bool filedialog::saveFiles(FDInfo& info, string_view caption, string_view extens wxFileDialog fd( parent, wxutil::strFromView(caption), - dir_last, + ui::getStateString("FileDialogLastDir"), "ignored", wxutil::strFromView(extensions), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); @@ -330,7 +323,7 @@ bool filedialog::saveFiles(FDInfo& info, string_view caption, string_view extens info.path = fd.GetDirectory(); // Set last dir - dir_last = info.path; + ui::saveStateString("FileDialogLastDir", info.path); return true; } @@ -357,13 +350,17 @@ filedialog::FDInfo filedialog::saveFiles(string_view caption, string_view extens string filedialog::openDirectory(string_view caption, wxWindow* parent) { // Open a directory browser dialog - wxDirDialog dialog_open(parent, wxutil::strFromView(caption), dir_last, wxDD_DIR_MUST_EXIST | wxDD_NEW_DIR_BUTTON); + wxDirDialog dialog_open( + parent, + wxutil::strFromView(caption), + ui::getStateString("FileDialogLastDir"), + wxDD_DIR_MUST_EXIST | wxDD_NEW_DIR_BUTTON); // Run the dialog if (dialog_open.ShowModal() == wxID_OK) { // Set last dir - dir_last = wxutil::strToView(dialog_open.GetPath()); + ui::saveStateString("FileDialogLastDir", dialog_open.GetPath().ToStdString()); return dialog_open.GetPath().ToStdString(); } diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index db3c9b888..bd386a942 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -1,6 +1,6 @@ # Use system packages if available -find_package(ZLIB REQUIRED) -find_package(BZip2 REQUIRED) +find_package(ZLIB) +find_package(BZip2) # Set in parent scope set(ZLIB_LIBRARY ${ZLIB_LIBRARY} PARENT_SCOPE) @@ -13,6 +13,10 @@ else() file(GLOB_RECURSE DUMB_SOURCES dumb/*.c) endif() +include_directories( + . +) + # Roll the rest up into a big ball and compile it by itself set(EXTERNAL_SOURCES ) @@ -20,6 +24,7 @@ file(GLOB_RECURSE EXTERNAL_SOURCES lzma/C/LzmaDec.c glad/src/*.c libdrawtext/*.c + xxhash/xxhash.c ${DUMB_SOURCES} ${SLADE_HEADERS} ) @@ -41,13 +46,16 @@ add_subdirectory(lunasvg/source) add_subdirectory(lunasvg/3rdparty/software) add_subdirectory(lunasvg/3rdparty/plutovg) +# Add SQLiteCpp +add_subdirectory(SQLiteCpp) + # Required for libdrawtext or we get multiple definition linker errors if (NOT MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fcommon") endif () add_library(external STATIC ${EXTERNAL_SOURCES}) -target_link_libraries(external ${ZLIB_LIBRARY} lunasvg fmt::fmt ${CMAKE_DL_LIBS}) +target_link_libraries(external ${ZLIB_LIBRARY} lunasvg fmt::fmt ${CMAKE_DL_LIBS} SQLiteCpp) set(EXTERNAL_LIBRARIES external PARENT_SCOPE) if(USE_SYSTEM_DUMB) diff --git a/thirdparty/SQLiteCpp/.cproject b/thirdparty/SQLiteCpp/.cproject new file mode 100644 index 000000000..2db68722e --- /dev/null +++ b/thirdparty/SQLiteCpp/.cproject @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/thirdparty/SQLiteCpp/.editorconfig b/thirdparty/SQLiteCpp/.editorconfig new file mode 100644 index 000000000..1fab73371 --- /dev/null +++ b/thirdparty/SQLiteCpp/.editorconfig @@ -0,0 +1,14 @@ +root = true + +# 4 space indentation +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +# 2 space indentation for CI configuration +[*.yml] +indent_style = space +indent_size = 2 diff --git a/thirdparty/SQLiteCpp/.gitbugtraq b/thirdparty/SQLiteCpp/.gitbugtraq new file mode 100644 index 000000000..9ec8c1aca --- /dev/null +++ b/thirdparty/SQLiteCpp/.gitbugtraq @@ -0,0 +1,7 @@ +# .gitbugtraq for Git GUIs (SmartGit/TortoiseGit) to show links to the Github issue tracker. +# Instead of the repository root directory, it could be added as an additional section to $GIT_DIR/config. +# (note that '\' need to be escaped). +[bugtraq] + url = https://github.com/SRombauts/SQLiteCpp/issues/%BUGID% + loglinkregex = "#\\d+" + logregex = \\d+ diff --git a/thirdparty/SQLiteCpp/.github/workflows/build.yml b/thirdparty/SQLiteCpp/.github/workflows/build.yml new file mode 100644 index 000000000..2ae05d0a1 --- /dev/null +++ b/thirdparty/SQLiteCpp/.github/workflows/build.yml @@ -0,0 +1,68 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - { + name: "Windows Latest MSVC", + os: windows-latest, + generator: "Visual Studio 17 2022", + build_type: "Debug", + cc: "cl", cxx: "cl", + extra_path: "", + } + - { + name: "Windows Latest MinGW", + os: windows-latest, + generator: "MinGW Makefiles", + build_type: "Debug", + cc: "gcc", cxx: "g++", + extra_path: "C:\\ProgramData\\chocolatey\\lib\\mingw\\tools\\install\\mingw64\\bin", + } + - { + name: "Ubuntu Latest GCC", + os: ubuntu-latest, + generator: "Unix Makefiles", + build_type: "Debug", + cc: "gcc", cxx: "g++", + extra_path: "" + } + - { + name: "macOS Latest Clang", + os: macos-latest, + generator: "Unix Makefiles", + build_type: "Debug", + cc: "clang", cxx: "clang++", + extra_path: "" + } + + steps: + - uses: actions/checkout@v3 + - name: submodule + run: git submodule update --init --recursive + - name: extra_path + shell: bash + run: echo "${{matrix.config.extra_path}}" >> $GITHUB_PATH + - name: configure + shell: cmake -P {0} + run: | + set(ENV{CC} ${{matrix.config.cc}}) + set(ENV{CXX} ${{matrix.config.cxx}}) + - name: generate + run: | + mkdir build + cd build + cmake -G "${{matrix.config.generator}}" -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=${{matrix.config.build_type}} -DSQLITECPP_BUILD_EXAMPLES=ON -DSQLITECPP_BUILD_TESTS=ON -DSQLITECPP_RUN_CPPCHECK=OFF -DSQLITECPP_RUN_CPPLINT=OFF .. + - name: build + run: cmake --build build --config ${{matrix.config.build_type}} + - name: test + run: | + cd build + ctest --verbose --output-on-failure diff --git a/thirdparty/SQLiteCpp/.github/workflows/meson.yaml b/thirdparty/SQLiteCpp/.github/workflows/meson.yaml new file mode 100644 index 000000000..392d29e6e --- /dev/null +++ b/thirdparty/SQLiteCpp/.github/workflows/meson.yaml @@ -0,0 +1,80 @@ +name: meson + +on: [push, pull_request] + +jobs: + build: + name: (Meson) ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - { + name: "Windows Latest MSVC", + os: windows-latest, + cc: "cl", cxx: "cl", + extra_path: "", + requires_msvc: true, + } + - { + name: "Windows Latest MinGW", + os: windows-latest, + cc: "gcc", cxx: "g++", + extra_path: "C:\\ProgramData\\chocolatey\\lib\\mingw\\tools\\install\\mingw64\\bin", + } + - { + name: "Windows Latest Clang", + os: windows-latest, + cc: "clang", cxx: "clang++", c_ld: "lld-link", cxx_ld: "lld-link", + extra_path: "", + } + - { + name: "Ubuntu Latest GCC", + os: ubuntu-latest, + cc: "gcc", cxx: "g++", + extra_path: "" + } + - { + name: "Ubuntu Latest Clang", + os: ubuntu-latest, + cc: "clang", cxx: "clang++", c_ld: "lld", cxx_ld: "lld", + extra_path: "" + } + - { + name: "macOS Latest Clang", + os: macos-latest, + cc: "clang", cxx: "clang++", + extra_path: "" + } + + steps: + - uses: actions/checkout@v3 + # use msvc-dev-cmd to setup the environment for MSVC if needed + - name: setup MSVC + if: matrix.config.requires_msvc + uses: ilammy/msvc-dev-cmd@v1 + - name: extra_path + shell: bash + run: echo "${{matrix.config.extra_path}}" >> $GITHUB_PATH + - name: install prerequisites + run: | + # asuming that python and pip are already installed + pip3 install meson ninja + - name: setup meson project + env: # set proper compilers and linkers for meson + CC: ${{matrix.config.cc}} + CXX: ${{matrix.config.cxx}} + C_LD: ${{matrix.config.c_ld}} + CXX_LD: ${{matrix.config.cxx_ld}} + run: | + # setup the build directory with tests and examples enabled + meson setup builddir -DSQLITECPP_BUILD_TESTS=true -DSQLITECPP_BUILD_EXAMPLES=true --force-fallback-for=sqlite3 + - name: build meson project + run: | + # build the project + meson compile -C builddir + - name: test + run: | + # run the tests + meson test -C builddir diff --git a/thirdparty/SQLiteCpp/.github/workflows/subdir_example.yml b/thirdparty/SQLiteCpp/.github/workflows/subdir_example.yml new file mode 100644 index 000000000..df3873e79 --- /dev/null +++ b/thirdparty/SQLiteCpp/.github/workflows/subdir_example.yml @@ -0,0 +1,43 @@ +name: subdir_example + +on: [push, pull_request] + +jobs: + build: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - { + name: "Windows Latest MSVC", + os: windows-latest, + build_type: "Debug", cc: "cl", cxx: "cl", + } + - { + name: "Ubuntu Latest GCC", + os: ubuntu-latest, + build_type: "Debug", cc: "gcc", cxx: "g++" + } + - { + name: "macOS Latest Clang", + os: macos-latest, + build_type: "Debug", cc: "clang", cxx: "clang++" + } + + steps: + - uses: actions/checkout@v3 + - name: configure + shell: cmake -P {0} + run: | + set(ENV{CC} ${{matrix.config.cc}}) + set(ENV{CXX} ${{matrix.config.cxx}}) + - name: generate + run: | + cd examples/example2 + mkdir build + cd build + cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=${{matrix.config.build_type}} .. + - name: build + run: cmake --build examples/example2/build --config ${{matrix.config.build_type}} diff --git a/thirdparty/SQLiteCpp/.gitignore b/thirdparty/SQLiteCpp/.gitignore new file mode 100644 index 000000000..3c48a9774 --- /dev/null +++ b/thirdparty/SQLiteCpp/.gitignore @@ -0,0 +1,34 @@ +Debug +Release +build +*.a + +# ignore clangd cache directory +.cache +.vs/ +.vscode/ +.vsconfig +*.sln +*.ncb +*.suo +*.user +*sdf +*.vc* +*~ +doc +core +*ipch +.settings/ + +# do not track Visual Studio CMake settings +CMakeSettings.json +CMakeCache.txt +CMakeFiles +*.dir +Testing +Win32 + +SQLiteCpp_example1 +SQLiteCpp_tests + +!FindSQLiteCpp.cmake diff --git a/thirdparty/SQLiteCpp/.gitmodules b/thirdparty/SQLiteCpp/.gitmodules new file mode 100644 index 000000000..8cf8b5e33 --- /dev/null +++ b/thirdparty/SQLiteCpp/.gitmodules @@ -0,0 +1,3 @@ +[submodule "googletest"] + path = googletest + url = https://github.com/google/googletest.git diff --git a/thirdparty/SQLiteCpp/.project b/thirdparty/SQLiteCpp/.project new file mode 100644 index 000000000..c3f149d58 --- /dev/null +++ b/thirdparty/SQLiteCpp/.project @@ -0,0 +1,85 @@ + + + SQLiteC++ + + + + + + org.python.pydev.PyDevBuilder + + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + ?name? + + + + org.eclipse.cdt.make.core.append_environment + true + + + org.eclipse.cdt.make.core.autoBuildTarget + all + + + org.eclipse.cdt.make.core.buildArguments + -j + + + org.eclipse.cdt.make.core.buildCommand + make + + + org.eclipse.cdt.make.core.cleanBuildTarget + clean + + + org.eclipse.cdt.make.core.contents + org.eclipse.cdt.make.core.activeConfigSettings + + + org.eclipse.cdt.make.core.enableAutoBuild + false + + + org.eclipse.cdt.make.core.enableCleanBuild + true + + + org.eclipse.cdt.make.core.enableFullBuild + true + + + org.eclipse.cdt.make.core.fullBuildTarget + all + + + org.eclipse.cdt.make.core.stopOnError + true + + + org.eclipse.cdt.make.core.useDefaultBuildCmd + true + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + org.python.pydev.pythonNature + + diff --git a/thirdparty/SQLiteCpp/.travis.yml b/thirdparty/SQLiteCpp/.travis.yml new file mode 100644 index 000000000..baf9a2d18 --- /dev/null +++ b/thirdparty/SQLiteCpp/.travis.yml @@ -0,0 +1,136 @@ +# Copyright (c) 2012-2023 Sebastien Rombauts (sebastien.rombauts@gmail.com) + +language: cpp + +# Use Linux unless specified otherwise +os: linux + +cache: + apt: true + +env: + global: + - BUILD_TYPE=Debug + - ASAN=ON + - INTERNAL_SQLITE=ON + - VALGRIND=OFF + +# Build variants (should test a reasonable number of combination of CMake options) +jobs: + include: + + ########################################################################## + # GCC on Linux + ########################################################################## + + # Coverity static code analysis + - dist: bionic + env: + - COVERITY_SCAN_PROJECT_NAME=SRombauts/SQLiteCpp + - COVERITY_SCAN_BRANCH_PATTERN=master + - COVERITY_SCAN_NOTIFICATION_EMAIL=sebastien.rombauts@gmail.com + - COVERITY_SCAN_BUILD_COMMAND_PREPEND="cmake ." + - COVERITY_SCAN_BUILD_COMMAND="make -j8" + # Encrypted COVERITY_SCAN_TOKEN, created via the "travis encrypt" command using the project repo's public key + - secure: "Qm4d8NEDPBtYZCYav46uPEvDCtaRsjLXlkVS+C+WCJAPcwXCGkrr96wEi7RWcq2xD86QCh0XiqaPT+xdUmlohOYIovRhaaBmZ1lwIJ4GsG/ZR6xoFr3DYsZ3o4GyXk2vNXNxEl82AC+Xs6e6gkLOV9XRkBcjpVIvoIXgNlKWeGY=" + + # GCC 7.4.0 Debug build with GCov for coverage build + - dist: bionic + env: + - cc=gcc cxx=g++ + - GCOV=ON + - COVERALLS=true + + # GCC 7.4.0 Debug build with Valgrind instead of Address Sanitizer + - dist: bionic + env: + - cc=gcc cxx=g++ + - ASAN=OFF + - VALGRIND=true + + # GCC 7.4.0 Release build + - dist: bionic + env: + - cc=gcc cxx=g++ + - BUILD_TYPE=Release + + # GCC 7.4.0 test linking with libsqlite3-dev package + - dist: bionic + env: + - cc=gcc cxx=g++ + - INTERNAL_SQLITE=OFF + + # GCC 5.4.0 + - dist: xenial + env: + - cc=gcc cxx=g++ + + # GCC 4.8.4 + - dist: trusty + env: + - cc=gcc cxx=g++ + + ########################################################################## + # Clang on Linux + ########################################################################## + + # Clang 7.0.0 + - dist: bionic + env: + - cc=clang cxx=clang++ + + # Clang 7.0.0 + - dist: xenial + env: + - cc=clang cxx=clang++ + + # Clang 5.0.0 + - dist: trusty + env: + - cc=clang cxx=clang++ + + ########################################################################## + # Clang on OSX + ########################################################################## + + # Latest XCode - AppleClang 9.1.0 + - os: osx + env: + - cc=clang cxx=clang++ + + # XCode 8.3 - AppleClang 8.1.0 + - os: osx + osx_image: xcode8.3 + env: + - cc=clang cxx=clang++ + +before_install: + # Coverity: don't use addons.coverity_scan since it run on every job of the build matrix, which waste resources and exhausts quotas + # Note: the job dedicated to Coverity need to only run the shell script and then exit (to not try to build and run unit tests etc.) + - if [[ -n "$COVERITY_SCAN_PROJECT_NAME" ]] ; then curl -s https://scan.coverity.com/scripts/travisci_build_coverity_scan.sh | bash ; exit 0 ; fi + + - if [[ "$INTERNAL_SQLITE" == "OFF" ]]; then sudo apt-get install libsqlite3-dev ; fi + - if [[ "$VALGRIND" == "true" ]]; then sudo apt-get install valgrind ; fi + - if [[ "$COVERALLS" == "true" ]]; then pip install --user cpp-coveralls ; fi + + # Set the compiler environment variables properly + - export CC=${cc} + - export CXX=${cxx} + +# scripts to run before build +before_script: + - mkdir build + - cd build + - cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSQLITECPP_INTERNAL_SQLITE=$INTERNAL_SQLITE -DSQLITECPP_USE_ASAN=$ASAN -DSQLITECPP_USE_GCOV=$GCOV -DSQLITECPP_BUILD_EXAMPLES=ON -DSQLITECPP_BUILD_TESTS=ON .. + +# build examples, and run tests (ie make & make test) +script: + - cmake --build . + - export ASAN_OPTIONS=verbosity=1:debug=1 + - ctest --verbose --output-on-failure + - if [[ "$VALGRIND" == "true" ]]; then valgrind --leak-check=full --error-exitcode=1 ./SQLiteCpp_example1 ; fi + - if [[ "$VALGRIND" == "true" ]]; then valgrind --leak-check=full --error-exitcode=1 ./SQLiteCpp_tests ; fi + +# generate and publish GCov coveralls results +after_success: + - if [[ "$COVERALLS" == "true" ]]; then coveralls --root .. -e examples -e googletest -e sqlite3 -e tests -E ".*feature_tests.*" -E ".*CompilerId.*" --gcov-options '\-lp' ; fi diff --git a/thirdparty/SQLiteCpp/CHANGELOG.md b/thirdparty/SQLiteCpp/CHANGELOG.md new file mode 100644 index 000000000..d376cbc29 --- /dev/null +++ b/thirdparty/SQLiteCpp/CHANGELOG.md @@ -0,0 +1,252 @@ +2012 Mar 30 +- Start of a new thin C++ SQLite wrapper + +2012 Apr 2 +- The wrapper is functional +- Added documentation and examples +- Publication on GitHub + +Version 0.1.0 - 2012 Apr 4 +- Added a Database::exec() method to execute simple SQL statement +- Added a version number like in sqlite3.h, starting with 0.1.0 + +Version 0.2.0 - 2012 Apr 11 +- Added getLastInsertId() and setBusyTimout() +- Added bind() by name methods + +Version 0.3.0 - 2012 Apr 16 +- Added an easy wrapper Database::execAngGet() + +Version 0.4.0 - 2012 Apr 23 +- Added a Database::tableExists() easy to use function + +Dec 10 2012 +- Added a Statement::exec() method to execute a one-step query with no expected result + +Version 0.5.0 - 2013 March 9 +- Added assert() on errors on destructors +- Added getBytes() +- Added getBlob(), getType() and isInteger/isFloat/isText/isBlob/isNull +- Added bind() for binary blob data + +Version 0.5.1 - 2013 April 7 +- Added Column::getName() + +Version 0.6.0 - 2013 November 22 +- Renamed Column::getName() to Column::getOriginName() +- Added Column::getName() + +Version 0.7.0 - 2014 January 9 +- Added Database::createFunction() +- Added std::string version of existing APIs +- Improved CMake with more build options and Doxygen auto-detection + +Version 0.8.0 - 2014 February 26 +- Database constructor support opening a database with a custom VFS (default to NULL) +- Changed Column::getText() to return empty string "" by default instead of NULL pointer (to handle std::string conversion) + +Version 1.0.0 - 2015 May 3 +- Public headers file moved to include/ dir +- Added support to biicode in CMakeLists.txt +- Added Unit Tests +- Added aBusyTimeoutMs parameter to Database() constructors +- Added Database::getTotalChanges() +- Added Database::getErrorCode() +- Added Statement::clearBindings() +- Added Statement::getColumn(aName) +- Added Statement::getErrorCode() +- Added Statement::getColumnName(aIndex) +- Added Statement::getColumnOriginName(aIndex) + +Version 1.1.0 - 2015 May 18 +- Fixed valgrind error on Database destructor +- Added Database::loadExtension + +Version 1.2.0 - 2015 September 9 +- Fixed build with GCC 5.1.0 +- Fixed MSVC release build warning +- Fixed CppDepends warnings +- Updated documentation on installation +- Added Database::getHandle() + +Version 1.3.0 - 2015 November 1 +- Fixed build with Visual Studio 2015 +- Further improvements to README +- Added Backup class + +Version 1.3.1 - 2016 February 10 +- Switch Linux/Mac build to the provided SQLite3 C library +- Update SQLite3 from 3.8.8.3 to latest 3.10.2 (2016-01-20) +- Remove warnings +- Remove biicode support (defunct service, servers will shutdown the 16th of February 2016) + +Version 2.0.0 - 2016 July 25 +- Update SQLite3 from 3.10.2 to latest 3.13 (2016-05-18) +- Move #include from headers to .cpp files only using forward declarations +- Add Database::VERSION to reach SQLITE_VERSION without including sqlite3.h in application code +- Add getLibVersion() and getLibVersionNumber() to get runtime version of the library +- Better exception messages when Statements fail PR #84 +- Variadic templates for bind() (C++14) PR #85 +- Add Statement::bindNoCopy() methods for strings, using SQLITE_STATIC to avoid internal copy by SQLite3 PR #86 +- Add Statement::bind() overload for uint32_t, and Column::getUint() and cast operator to uint32_t PR #86 +- Use the new SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION from SQLite 3.13 for security reason +- Rename Backup::remainingPageCount()/totalPageCount() to Backup::getRemainingPageCount()/getTotalPageCount() +- Remove Column::errmsg() method : use Database or Statement equivalents +- More unit tests, with code coverage status on the GitHub page +- Do not force MSVC to use static runtime if unit-tests are not build + +Version 2.1.0 - 2017 July 18 +- Update SQLite3 from 3.13 to latest 3.19.3 (2017-06-08) +- Fixed Incompatibility in 3.19.0 (to use older SQLite version set the CMake variable SQLITE_USE_LEGACY_STRUCT) #125 +- Fixed link error (inline in cpp) and compiler warnings (unused variable...) #96 +- Added ability to open encrypted databases (using SQLCipher, eg. libsqlcipher-dev) #107 +- Added convenience functions for constructing objects from a row #114 +- Added CMake install step #118 +- Fix warnings #119 +- Make cpplint.py Python-3 compatible #120 +- Link libssp when targeted #100 +- Removed redundant const #102 + +Version 2.2.0 - 2017 Sept 19 +- Update SQLite3 from 3.19.3 to latest 3.20.1 (2017-08-24) #143 +- Added tryExecuteStep and tryReset #142 +- Removed virtual keywords from destructors #140 +- Removed misplaced noexcept keyword #139 +- Improved Exception class C++ conformance #138 +- Fix warnings #134 +- Deprecated Statement::isOk() to Statement::hasRow() + +Version 2.3.0 - 2019 March 3 +- Update SQLite3 from 3.20.1 to latest 3.27.2 (2019-02-25) #183 #187 +- Add Statement binding for long int values #147 +- Allows long int for bind when used with name #148 +- More cmake instructions for Linux #151 +- Add comparison with sqlite_orm #141 +- Fix Statement::bind truncates long integer to 32 bits on x86_64 Linux #155 +- Add a move constructor to Database #157 +- Added tests for all MSVC compilers available on AppVeyor (2013, 2015, 2017) #169 +- Update VariadicBind.h #172 +- Better CMake compatibility #170 +- Add implicit cast operator to char and short types #179 #180 + +Version 2.4.0 - 2019 August 25 +- Update SQLite3 from 3.27.2 to 3.29.0 (2019-07-10) #217 +- #191 CMake Warning line 299 +- #190 Implement move constructors +- #192 Add wrapper for bind parameter count +- #197 Add tuple_bind and execute_many (requested by #24) +- #199 Fix #156 misleading error message in exception from Statement::exec +- #201 Add Statement::getExpandedSQL() to get the SQL text of prepared statement with bound parameters expanded +- #211 Implement Database::backup() +- #215 Disable implicit fallthrough warning when building internal sqlite3 +- #216 Set PROJECT_VERSION to fix CMP0048 Policy warnings + +Version 2.5.0 - 2019 December 31 +- Update SQLite3 from 3.29.0 to 3.30.1 (2019-10-10) +- 100% Unit Test coverage +- #212 fix sqlite3 compile properties (jzt) +- #219 Disable cast-function-type warning when building internal sqlite (zxey) +- #230 Fixed installation on other than Ubuntu GNU/Linux distributions (xvitaly) +- #228 use transitive compile definitions via cmake (BioDataAnalysis/emmenlau) +- #232 Added support of packaged GTest for running unit tests (xvitaly) +- #231 Added SOVERSION field for shared library (xvitaly) +- #229 Explicitly find and link against system sqlite library (xvitaly) +- #235 Added support for cmake dependencies and version information (BioDataAnalysis/emmenlau) +- #249 Added SQLite header parsing functionality and associated tests (patrick--) + +- #251 Added example for getHeaderInfo() + +Version 3.0.0 - 2020 January 31 +- C++11 is now required +- CMake 3.1 minimum +- Visual Studio 2015 minimum +- Update Googletest to latest release 1.10 +- Add Github Actions continuous integration solution +- Add Valgrind memcheck tool to Travis CI +- Remove Statement::isOk() deprecated in 2.2.0 when renamed to Statement::hasRow() +- Replace Database::backup() "C" implementation by calling the Backup class +- #252 Run Valgrind memcheck on Travis CI +- #253 Keep inline functions for GCov code coverage +- #254 Re-enable Coverity static analysis +- #256 Fix linking with system library (libsqlite3) +- #242 Added a `getIndex` method and used it (KOLANICH) +- #257 Improve Statement unit tests coverage (bind by name with a std::string) +- #234 support for external sqlite3 (BioDataAnalysis/emmenlau) +- #243 adding a pure attribute to getIndex() (KOLANICH) + +Version 3.1.0 - 2020 August 11 +- Update SQLite3 from 3.30.1 to 3.32.3 (2020-06-18) +- #274 Install both cmake files into same lib directory from tcraigtyler +- #275 Add a method on Statement to get the declared type of a column. from daniel-schmidt +- #284 Add SQLITE_OPEN_FULLMUTEX flag from rwrx +- #286 Add CMake option to toggle stack protection from chrisdalke +- #287 Fixed installation on other than Ubuntu distributions from xvitaly +- #288 Allow building of sqlite JSON1 extension when building internal sqlite library from zxey + +Version 3.1.1 - 2020 August 19 +- #292 Fix compilation if using SQLITE_HAS_CODEC from sum01 +- #293 Remove FindSQLiteCpp.cmake from sum01 + +Version 3.2.0 - 2022 Septembre 18 +- #300 #316 #362 #368 Updated SQLite3 from 3.32.3 to 3.39.3 (2022-09-05) +- #236 Disable explicit setting of MSVC runtime from BioDataAnalysis/emmenlau +- #308 Fix build warning due to string truncation from stauffer-garmin +- #311 Add Database::tryExec() from kcowolf +- #313 [CMake] Add SQLITECPP_INCLUDE_SCRIPT option from past-due +- #314 Add Database constructor for filesystem::path (#296) from ptrks +- #295 Compile internal SQLite library with -ffunction-sections from smichaku +- #299 Added Savepoint support from catalogm +- #333 Added Database and Statement getChanges() +- #305 Add other constants that work with sqlite3_open_v2 from LuAPi/more-flags +- #333 Added Database and Statement method getChanges() from SRombauts/get-changes +- #334 fix link for HAS_CODEC from linux-fan-dave/master +- #338 fix load extension from paulo-coutinho/fix-load-extension +- #335 from jagerman/older-macos-avoid-std-filesystem +- #337 Add catkin configuration from ardabbour/master +- #339 Allow specifying transaction behaviors DEFERRED, IMMEDIATE, and EXCLUSIVE from jjenkins278/transaction_behavior +- #340 add HTML keywords and properly link up the links in docs/README.md from phoebe-leong/patch-1 +- #341 Install the package.xml file from ardabbour/patch-1 +- #352 add basic meson support from ninjaoflight/meson-support +- #349 Refactoring of Statement and Column classes from Kacperos155/refactoring-Statement&Column +- #359 Fix compilation issues earlier than iOS 13 +- #354 Windows improved support (meson) from ninjaoflight/windows-migration +- #361 Fix Statement unit test using long from SRombauts/fix-statement-unit-tests-long-long-type +- #346 Add compatible definition for std::experimental::filesystem from guoh27/master +- #364 Removal of remaining long APIs from SRombauts/convert-remaining-long-types +- #366 Add vcpkg installation instructions from FrankXie05/vcpkg-instructions +- #360 Small improvements and code cleaning from Kacperos155/small_improvements + +Versions 3.2.1 - 2022 Decembre 12 +- Merge pull request #383 Update SQLite from 3.39.3 to 3.40.0 (2022-11-16) from SRombauts/update-sqlite-340 +- Merge pull request #370 Don't link anymore with Visual Studio's static runtime by default from SRombauts/dont-enforce-static-linking +- Merge pull request #371 from SRombauts/appveyor-vs-2022 +- Merge pull request #277 from cuberite/cmake-scoping +- Merge pull request #374 Update googletest from vuhailongkl97/master +- Merge pull request #377 Some documentation fixes from cbielow/fix_doc +- Merge pull request #380 [Meson] fixes for meson project from ninjaoflight/windows-support +- Merge pull request #387 Ensure that TEXT column is UTF-8 encoded before using sqlite3_column_blob() from dougnazar +- Merge pull request #385 disable SQLITECPP_USE_STACK_PROTECTION when on MinGW from SRombauts/mingw-disable-stack-protection +- Merge pull request #386 [meson] Update SQLite from 3.39.3 to 3.40.0 from ninjaoflight/sqlite-meson-update +- Merge pull request #389 [meson] add missing compile options from ninjaoflight/meson-fixes + +Version 3.3.0 - 2023 May 24 +- Merge pull request #393 Fix preprocessor issues from jowr/fix_preprocessor_issues +- Merge pull request #394 check if SQLITE_OPEN_NOFOLLOW is defined from ninjaoflight/macos-11-fix +- Merge pull request #391 meson project changes based on wrap submission review from ninjaoflight/meson-macos-fix +- Merge pull request #390 fix incorrect work of savepoint from spoyler/save_point Sébastien Rombauts 12/15/2022 01:12 PM +- Merge pull request #396 Rename Savepoint RollbackTo() and fix class comments and formatting from SRombauts/rename-savepoint-rollback-to +- Merge pull request #384 Add Mingw GitHub actions from SRombauts/mingw-github-actions +- Merge pull request #397 Add a Transaction::rollback() method from SRombauts/add-transaction-rollback +- Merge pull request #395 add meson usage guide from ninjaoflight/meson-readme-guide +- Merge pull request #401 Fix meson installation from dougnazar/fix_meson_install +- Merge pull request #400 CMakr/meson Lint corrections from ninjaoflight/lint-corrections +- Merge pull request #404 Add documentation for prepared statements in transactions from ewarchul/query_transactions_example +- Merge pull request #399 add disable option for sqlite3_expanded_sql from ninjaoflight/optional-sqlite3_expanded_sql +- Merge pull request #408 correct executable name in meson from ninjaoflight/patch-2 +- Merge pull request #407 Create Meson CI from ninjaoflight/patch-1 +- Merge pull request #409 Update package.xml from poshul/patch-1 +- Merge pull request #410 use checkout@v3 in CMake CI from ninjaoflight/fix-nodejs-warnings +- Merge pull request #406 Dllexport import from pierre-aimi/dllexport_import +- Merge pull request #415 Remove mismatched else condition in CMakeLists.txt from Timmmm/patch-1 +- Merge pull request #413 Fix compiler warnings warning from ninjaoflight/fix-visibility-warning +- Merge pull request #423 Update SQLite from 3.40.0 to 3.42.0 (2023-05-16) from SRombauts/update-sqlite diff --git a/thirdparty/SQLiteCpp/CMakeLists.txt b/thirdparty/SQLiteCpp/CMakeLists.txt new file mode 100644 index 000000000..df5693d9a --- /dev/null +++ b/thirdparty/SQLiteCpp/CMakeLists.txt @@ -0,0 +1,475 @@ +# Main CMake file for compiling the library itself, examples and tests. +# +# Copyright (c) 2012-2023 Sebastien Rombauts (sebastien.rombauts@gmail.com) +# +# Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +# or copy at http://opensource.org/licenses/MIT) +cmake_minimum_required(VERSION 3.1) # for "CMAKE_CXX_STANDARD" version +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # custom CMake modules like FindSQLiteCpp +project(SQLiteCpp VERSION 3.3.0) + +# SQLiteC++ 3.x requires C++11 features +if (NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 11) +elseif (CMAKE_CXX_STANDARD LESS 11) + message(WARNING "CMAKE_CXX_STANDARD has been set to '${CMAKE_CXX_STANDARD}' which is lower than the minimum required standard (c++11).") +endif () +message(STATUS "Using c++ standard c++${CMAKE_CXX_STANDARD}") +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +message (STATUS "CMake version: ${CMAKE_VERSION}") +message (STATUS "Project version: ${PROJECT_VERSION}") + +option(SQLITECPP_BUILD_TESTS "Build and run tests." OFF) + +# Define useful variables to handle OS differences: +if (WIN32) + set(DEV_NULL "NUL") +else (WIN32) # UNIX + set(DEV_NULL "/dev/null") +endif (WIN32) + +# then Compiler/IDE differences: +if (MSVC) + set(CPPLINT_ARG_OUTPUT "--output=vs7") + set(CPPCHECK_ARG_TEMPLATE "--template=vs") + # disable Visual Studio warnings for fopen() used in the example + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + # Flags for linking with multithread static C++ runtime, required by internal googletest + option(SQLITECPP_USE_STATIC_RUNTIME "Use MSVC static runtime (default for internal googletest)." ${SQLITECPP_BUILD_TESTS}) + if (SQLITECPP_USE_STATIC_RUNTIME) + message(STATUS "Linking against multithread static C++ runtime") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT") + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") + endif (SQLITECPP_USE_STATIC_RUNTIME) + # MSVC versions prior to 2015 are not supported anymore by SQLiteC++ 3.x + if (MSVC_VERSION LESS 1900) # OR MSVC_TOOLSET_VERSION LESS 140) + message(ERROR "Visual Studio prior to 2015 is not supported anymore.") + endif (MSVC_VERSION LESS 1900) +else (MSVC) # Unix/macOS/MinGW + set(CPPLINT_ARG_OUTPUT "--output=eclipse") + set(CPPCHECK_ARG_TEMPLATE "--template=gcc") + # Useful compile flags and extra warnings + if (NOT MINGW) + # Stack protection is not supported on MinGW-W64 on Windows. + # Allow this flag to be turned off. + option(SQLITECPP_USE_STACK_PROTECTION "USE Stack Protection hardening." ON) + if (SQLITECPP_USE_STACK_PROTECTION) + message (STATUS "Using Stack Protection hardening") + add_compile_options(-fstack-protector) + endif (SQLITECPP_USE_STACK_PROTECTION) + endif (NOT MINGW) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -Wswitch-enum -Wshadow -Wno-long-long") # C++ only, don't bother with sqlite3 + if (CMAKE_COMPILER_IS_GNUCXX) + # GCC flags + option(SQLITECPP_USE_GCOV "USE GCov instrumentation." OFF) + if (SQLITECPP_USE_GCOV) + message (STATUS "Using GCov instrumentation") + add_compile_options (-coverage) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -coverage") + endif () + endif (CMAKE_COMPILER_IS_GNUCXX) +endif (MSVC) +# and then common variables +set(CPPLINT_ARG_VERBOSE "--verbose=3") +set(CPPLINT_ARG_LINELENGTH "--linelength=120") + +# Print CXX compiler information +message (STATUS "CMAKE_CXX_COMPILER '${CMAKE_CXX_COMPILER}' '${CMAKE_CXX_COMPILER_ID}' '${CMAKE_CXX_COMPILER_VERSION}'") + +# Print CXX FLAGS +message (STATUS "CMAKE_CXX_FLAGS '${CMAKE_CXX_FLAGS}'") +if (MSVC) + message (STATUS "CMAKE_CXX_FLAGS_DEBUG '${CMAKE_CXX_FLAGS_DEBUG}'") + message (STATUS "CMAKE_CXX_FLAGS_RELEASE '${CMAKE_CXX_FLAGS_RELEASE}'") + message (STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO '${CMAKE_CXX_FLAGS_RELWITHDEBINFO}'") + message (STATUS "CMAKE_CXX_FLAGS_MINSIZEREL '${CMAKE_CXX_FLAGS_MINSIZEREL}'") +else () + if (CMAKE_BUILD_TYPE STREQUAL Debug) + message (STATUS "CMAKE_CXX_FLAGS_DEBUG '${CMAKE_CXX_FLAGS_DEBUG}'") + elseif (CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo) + message (STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO '${CMAKE_CXX_FLAGS_RELWITHDEBINFO}'") + elseif (CMAKE_BUILD_TYPE STREQUAL MinSizeRel) + message (STATUS "CMAKE_CXX_FLAGS_MINSIZEREL '${CMAKE_CXX_FLAGS_MINSIZEREL}'") + else () + message (STATUS "CMAKE_CXX_FLAGS_RELEASE '${CMAKE_CXX_FLAGS_RELEASE}'") + endif () +endif () + +## Build the C++ Wrapper ## + +# adding a new file require explicitly modifying the CMakeLists.txt +# so that CMake knows that it should rebuild the project (it is best practice) + +# list of sources files of the library +set(SQLITECPP_SRC + ${PROJECT_SOURCE_DIR}/src/Backup.cpp + ${PROJECT_SOURCE_DIR}/src/Column.cpp + ${PROJECT_SOURCE_DIR}/src/Database.cpp + ${PROJECT_SOURCE_DIR}/src/Exception.cpp + ${PROJECT_SOURCE_DIR}/src/Savepoint.cpp + ${PROJECT_SOURCE_DIR}/src/Statement.cpp + ${PROJECT_SOURCE_DIR}/src/Transaction.cpp +) +source_group(src FILES ${SQLITECPP_SRC}) + +# list of header files of the library +set(SQLITECPP_INC + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/SQLiteCpp.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Assertion.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Backup.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Column.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Database.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Exception.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Savepoint.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Statement.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Transaction.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/VariadicBind.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/ExecuteMany.h +) +source_group(include FILES ${SQLITECPP_INC}) + +# list of test files of the library +set(SQLITECPP_TESTS + tests/Column_test.cpp + tests/Database_test.cpp + tests/Savepoint_test.cpp + tests/Statement_test.cpp + tests/Backup_test.cpp + tests/Transaction_test.cpp + tests/VariadicBind_test.cpp + tests/Exception_test.cpp + tests/ExecuteMany_test.cpp +) +source_group(tests FILES ${SQLITECPP_TESTS}) + +# list of example files of the library +set(SQLITECPP_EXAMPLES + examples/example1/main.cpp +) +source_group(example1 FILES ${SQLITECPP_EXAMPLES}) + +# list of doc files of the library +set(SQLITECPP_DOC + README.md + LICENSE.txt + CHANGELOG.md + TODO.txt +) +source_group(doc FILES ${SQLITECPP_DOC}) + +option(SQLITECPP_INCLUDE_SCRIPT "Include config & script files." ON) +if (SQLITECPP_INCLUDE_SCRIPT) + # list of config & script files of the library + set(SQLITECPP_SCRIPT + .editorconfig + .gitbugtraq + .github/workflows/build.yml + .github/workflows/subdir_example.yml + .gitignore + .gitmodules + .travis.yml + appveyor.yml + build.bat + build.sh + cpplint.py + Doxyfile + cmake/FindSQLite3.cmake + cmake/SQLiteCppConfig.cmake.in + ) + source_group(scripts FILES ${SQLITECPP_SCRIPT}) +endif() + +# add sources of the wrapper as a "SQLiteCpp" static library +add_library(SQLiteCpp ${SQLITECPP_SRC} ${SQLITECPP_INC} ${SQLITECPP_DOC} ${SQLITECPP_SCRIPT}) + +# Options relative to SQLite and SQLiteC++ functions + +option(SQLITE_ENABLE_COLUMN_METADATA "Enable Column::getColumnOriginName(). Require support from sqlite3 library." ON) +if (SQLITE_ENABLE_COLUMN_METADATA) + # Enable the use of SQLite column metadata and Column::getColumnOriginName() method, + # Require that the sqlite3 library is also compiled with this flag (default under Debian/Ubuntu, but not on Mac OS X). + target_compile_definitions(SQLiteCpp PUBLIC SQLITE_ENABLE_COLUMN_METADATA) +endif (SQLITE_ENABLE_COLUMN_METADATA) + +option(SQLITE_ENABLE_ASSERT_HANDLER "Enable the user definition of a assertion_failed() handler." OFF) +if (SQLITE_ENABLE_ASSERT_HANDLER) + # Enable the user definition of a assertion_failed() handler (default to false, easier to handler for beginners). + target_compile_definitions(SQLiteCpp PUBLIC SQLITECPP_ENABLE_ASSERT_HANDLER) +endif (SQLITE_ENABLE_ASSERT_HANDLER) + +option(SQLITE_HAS_CODEC "Enable database encryption API. Not available in the public release of SQLite." OFF) +if (SQLITE_HAS_CODEC) + # Enable database encryption API. Requires implementations of sqlite3_key & sqlite3_key_v2. + # Eg. SQLCipher (libsqlcipher-dev) is an SQLite extension that provides 256 bit AES encryption of database files. + target_compile_definitions(SQLiteCpp PUBLIC SQLITE_HAS_CODEC) +endif (SQLITE_HAS_CODEC) + +option(SQLITE_USE_LEGACY_STRUCT "Fallback to forward declaration of legacy struct sqlite3_value (pre SQLite 3.19)" OFF) +if (SQLITE_USE_LEGACY_STRUCT) + # Force forward declaration of legacy struct sqlite3_value (pre SQLite 3.19) + target_compile_definitions(SQLiteCpp PUBLIC SQLITE_USE_LEGACY_STRUCT) +endif (SQLITE_USE_LEGACY_STRUCT) + +if(BUILD_SHARED_LIBS) + if(WIN32) + add_definitions("-DSQLITECPP_COMPILE_DLL") + target_compile_definitions(SQLiteCpp PRIVATE "SQLITECPP_DLL_EXPORT") + endif() +endif() + +option(SQLITE_OMIT_LOAD_EXTENSION "Enable omit load extension" OFF) +if (SQLITE_OMIT_LOAD_EXTENSION) + # Enable the user definition of load_extension(). + target_compile_definitions(SQLiteCpp PUBLIC SQLITE_OMIT_LOAD_EXTENSION) +endif (SQLITE_OMIT_LOAD_EXTENSION) + +if (UNIX AND (CMAKE_COMPILER_IS_GNUCXX OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) + set_target_properties(SQLiteCpp PROPERTIES COMPILE_FLAGS "-fPIC") +endif (UNIX AND (CMAKE_COMPILER_IS_GNUCXX OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) + +option(SQLITECPP_USE_ASAN "Use Address Sanitizer." OFF) +if (SQLITECPP_USE_ASAN) + if ((CMAKE_CXX_COMPILER_VERSION GREATER_EQUAL 6) OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) + message (STATUS "Using Address Sanitizer") + set_target_properties(SQLiteCpp PROPERTIES COMPILE_FLAGS "-fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") + if (CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold") + endif () + endif () +endif (SQLITECPP_USE_ASAN) + +if (SQLITECPP_USE_GCOV) + # Prevent the compiler from removing the unused inline functions so that they get tracked as "non-covered" + set_target_properties(SQLiteCpp PROPERTIES COMPILE_FLAGS "-fkeep-inline-functions -fkeep-static-functions") +endif () + +## Build provided copy of SQLite3 C library ## + +option(SQLITECPP_INTERNAL_SQLITE "Add the internal SQLite3 source to the project." ON) +if (SQLITECPP_INTERNAL_SQLITE) + message(STATUS "Compile sqlite3 from source in subdirectory") + option(SQLITE_ENABLE_JSON1 "Enable JSON1 extension when building internal sqlite3 library." ON) + # build the SQLite3 C library (for ease of use/compatibility) versus Linux sqlite3-dev package + add_subdirectory(sqlite3) + target_link_libraries(SQLiteCpp PUBLIC SQLite::SQLite3) +else (SQLITECPP_INTERNAL_SQLITE) + # When using the SQLite codec, we need to link against the sqlcipher lib & include + # So this gets the lib & header, and links/includes everything + if(SQLITE_HAS_CODEC) + # Make PkgConfig optional since Windows doesn't usually have it installed. + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + # IMPORTED_TARGET was added in 3.6.3 + if(CMAKE_VERSION VERSION_LESS 3.6.3) + pkg_check_modules(sqlcipher REQUIRED sqlcipher) + # Only used in Database.cpp so PRIVATE to hide from end-user + # Since we can't use IMPORTED_TARGET on this older Cmake version, manually link libs & includes + target_link_libraries(SQLiteCpp PRIVATE ${sqlcipher_LIBRARIES}) + target_include_directories(SQLiteCpp PRIVATE ${sqlcipher_INCLUDE_DIRS}) + else() + pkg_check_modules(sqlcipher REQUIRED IMPORTED_TARGET sqlcipher) + # Only used in Database.cpp so PRIVATE to hide from end-user + target_link_libraries(SQLiteCpp PRIVATE PkgConfig::sqlcipher) + endif() + else() + # Since we aren't using pkgconf here, find it manually + find_library(sqlcipher_LIBRARY "sqlcipher") + find_path(sqlcipher_INCLUDE_DIR "sqlcipher/sqlite3.h" + PATH_SUFFIXES + "include" + "includes" + ) + # Hides it from the GUI + mark_as_advanced(sqlcipher_LIBRARY sqlcipher_INCLUDE_DIR) + if(NOT sqlcipher_INCLUDE_DIR) + message(FATAL_ERROR "${PROJECT_NAME} requires the \"\" header to use the codec functionality but it wasn't found.") + elseif(NOT sqlcipher_LIBRARY) + message(FATAL_ERROR "${PROJECT_NAME} requires the sqlcipher library to use the codec functionality but it wasn't found.") + endif() + # Only used in Database.cpp so PRIVATE to hide from end-user + target_include_directories(SQLiteCpp PRIVATE "${sqlcipher_INCLUDE_DIR}/sqlcipher") + target_link_libraries(SQLiteCpp PRIVATE ${sqlcipher_LIBRARY}) + endif() + else() + find_package (SQLite3 REQUIRED) + message(STATUS "Link to sqlite3 system library") + target_link_libraries(SQLiteCpp PUBLIC SQLite::SQLite3) + if(SQLite3_VERSION VERSION_LESS "3.19") + set_target_properties(SQLiteCpp PROPERTIES COMPILE_FLAGS "-DSQLITECPP_HAS_MEM_STRUCT") + endif() + endif() +endif (SQLITECPP_INTERNAL_SQLITE) + +## disable the optional support for std::filesystem (C++17) +option(SQLITECPP_DISABLE_STD_FILESYSTEM "Disable the use of std::filesystem in SQLiteCpp." OFF) +if (SQLITECPP_DISABLE_STD_FILESYSTEM) + message (STATUS "Disabling std::filesystem support") + target_compile_definitions(SQLiteCpp PUBLIC SQLITECPP_DISABLE_STD_FILESYSTEM) +endif (SQLITECPP_DISABLE_STD_FILESYSTEM) + +## disable the optional support for sqlite3_expanded_sql (from sqlite3 3.14.0) +option(SQLITECPP_DISABLE_EXPANDED_SQL "Disable the use of sqlite3_expanded_sql in SQLiteCpp." OFF) +if (SQLITECPP_DISABLE_EXPANDED_SQL) + message (STATUS "Disabling sqlite3_expanded_sql support") + target_compile_definitions(SQLiteCpp PUBLIC SQLITECPP_DISABLE_EXPANDED_SQL) +endif (SQLITECPP_DISABLE_EXPANDED_SQL) + +# Link target with pthread and dl for Unix +if (UNIX) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + target_link_libraries(SQLiteCpp PUBLIC Threads::Threads ${CMAKE_DL_LIBS}) +endif (UNIX) + +# Set includes for target and transitive downstream targets + +target_include_directories(SQLiteCpp + PUBLIC + $ + $) + +# Allow the library to be installed via "make install" and found with "find_package" + +include(GNUInstallDirs) +install(TARGETS SQLiteCpp + EXPORT ${PROJECT_NAME}Targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT libraries) +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT headers FILES_MATCHING REGEX ".*\\.(hpp|h)$") +install(EXPORT ${PROJECT_NAME}Targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) +install(FILES ${PROJECT_SOURCE_DIR}/package.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + cmake/${PROJECT_NAME}ConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY AnyNewerVersion) +configure_package_config_file( + cmake/${PROJECT_NAME}Config.cmake.in + cmake/${PROJECT_NAME}Config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) + +# Optional additional targets: + +option(SQLITECPP_RUN_CPPLINT "Run cpplint.py tool for Google C++ StyleGuide." ON) +if (SQLITECPP_RUN_CPPLINT) + find_package(PythonInterp) + if (PYTHONINTERP_FOUND) + # add a cpplint target to the "all" target + add_custom_target(SQLiteCpp_cpplint + ALL + COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/cpplint.py ${CPPLINT_ARG_OUTPUT} ${CPPLINT_ARG_VERBOSE} ${CPPLINT_ARG_LINELENGTH} ${SQLITECPP_SRC} ${SQLITECPP_INC} + ) + endif (PYTHONINTERP_FOUND) +else (SQLITECPP_RUN_CPPLINT) + message(STATUS "SQLITECPP_RUN_CPPLINT OFF") +endif (SQLITECPP_RUN_CPPLINT) + +option(SQLITECPP_RUN_CPPCHECK "Run cppcheck C++ static analysis tool." ON) +if (SQLITECPP_RUN_CPPCHECK) + find_program(CPPCHECK_EXECUTABLE NAMES cppcheck) + if (CPPCHECK_EXECUTABLE) + # add a cppcheck target to the "all" target + add_custom_target(SQLiteCpp_cppcheck + ALL + COMMAND ${CPPCHECK_EXECUTABLE} -j 8 cppcheck --enable=style --quiet ${CPPCHECK_ARG_TEMPLATE} ${PROJECT_SOURCE_DIR}/src + ) + execute_process(COMMAND "${CPPCHECK_EXECUTABLE}" --version OUTPUT_VARIABLE CPPCHECK_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Found Cppcheck: ${CPPCHECK_EXECUTABLE} ${CPPCHECK_VERSION}") + else (CPPCHECK_EXECUTABLE) + message(STATUS "Could NOT find cppcheck") + endif (CPPCHECK_EXECUTABLE) +else (SQLITECPP_RUN_CPPCHECK) + message(STATUS "SQLITECPP_RUN_CPPCHECK OFF") +endif (SQLITECPP_RUN_CPPCHECK) + +option(SQLITECPP_RUN_DOXYGEN "Run Doxygen C++ documentation tool." OFF) +if (SQLITECPP_RUN_DOXYGEN) + find_package(Doxygen) + if (DOXYGEN_FOUND) + # add a Doxygen target to the "all" target + add_custom_target(SQLiteCpp_doxygen + ALL + COMMAND doxygen Doxyfile > ${DEV_NULL} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) + endif (DOXYGEN_FOUND) +else (SQLITECPP_RUN_DOXYGEN) + message(STATUS "SQLITECPP_RUN_DOXYGEN OFF") +endif (SQLITECPP_RUN_DOXYGEN) + +option(SQLITECPP_BUILD_EXAMPLES "Build examples." OFF) +if (SQLITECPP_BUILD_EXAMPLES) + + # add the basic example executable + add_executable(SQLiteCpp_example1 ${SQLITECPP_EXAMPLES}) + + target_link_libraries(SQLiteCpp_example1 SQLiteCpp) + if (MSYS OR MINGW) + target_link_libraries(SQLiteCpp_example1 ssp) + endif () +else (SQLITECPP_BUILD_EXAMPLES) + message(STATUS "SQLITECPP_BUILD_EXAMPLES OFF") +endif (SQLITECPP_BUILD_EXAMPLES) + +if (SQLITECPP_BUILD_TESTS) + # add the unit test executable + add_executable(SQLiteCpp_tests ${SQLITECPP_TESTS}) + target_link_libraries(SQLiteCpp_tests SQLiteCpp) + + find_package(GTest) + if (GTEST_FOUND) + message(STATUS "Link to GTest system library") + target_link_libraries(SQLiteCpp_tests GTest::GTest GTest::Main) + else (GTEST_FOUND) + message(STATUS "Compile googletest from source in submodule") + # deactivate some warnings for compiling the googletest library + if (NOT MSVC) + add_compile_options(-Wno-switch-enum) + endif (NOT MSVC) + + # add the subdirectory containing the CMakeLists.txt for the googletest library + if (NOT EXISTS "${PROJECT_SOURCE_DIR}/googletest/CMakeLists.txt") + message(FATAL_ERROR "Missing 'googletest' submodule! Either use 'git submodule init' and 'git submodule update' to get googletest according to the README, or deactivate unit tests with -DSQLITECPP_BUILD_TESTS=OFF") + endif () + add_subdirectory(googletest) + include_directories("${PROJECT_SOURCE_DIR}/googletest/googletest/include") + + # Add definitions to keep googletest from making the compilation fail + if (MSVC) + if (MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS_EQUAL 1919) # OR MSVC_TOOLSET_VERSION EQUAL 141) + target_compile_definitions(gtest PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + target_compile_definitions(gtest_main PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + target_compile_definitions(gmock PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + target_compile_definitions(gmock_main PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + endif (MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS_EQUAL 1919) + endif (MSVC) + + target_link_libraries(SQLiteCpp_tests gtest_main) + endif (GTEST_FOUND) + + # add a "test" target: + enable_testing() + + # does the tests pass? + add_test(UnitTests SQLiteCpp_tests) + + if (SQLITECPP_BUILD_EXAMPLES) + # does the example1 runs successfully? + add_test(Example1Run SQLiteCpp_example1) + endif (SQLITECPP_BUILD_EXAMPLES) +else (SQLITECPP_BUILD_TESTS) + message(STATUS "SQLITECPP_BUILD_TESTS OFF") +endif (SQLITECPP_BUILD_TESTS) + +# API version for SQLiteCpp shared library. +set_property(TARGET SQLiteCpp PROPERTY SOVERSION 0) diff --git a/thirdparty/SQLiteCpp/Doxyfile b/thirdparty/SQLiteCpp/Doxyfile new file mode 100644 index 000000000..749b68ab0 --- /dev/null +++ b/thirdparty/SQLiteCpp/Doxyfile @@ -0,0 +1,2525 @@ +# Doxyfile 1.8.16 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = SQLiteC++ + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 3.3.0 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "SQLiteC++ is a smart and easy to use C++ SQLite3 wrapper." + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 7 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat +# .inc files as Fortran files (default is PHP), and .f files as C (default is +# Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# (including Cygwin) ands Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = NO + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = YES + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file($line): $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = src \ + include + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.cpp \ + *.h + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = NO + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the +# cost of reduced performance. This can be particularly helpful with template +# rich C++ code for which doxygen's built-in parser lacks the necessary type +# information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files +# were built. This is equivalent to specifying the "-p" option to a clang tool, +# such as clang-check. These options will then be passed to the parser. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via Javascript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have Javascript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: https://developer.apple.com/xcode/), introduced with OSX +# 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://www.mathjax.org/mathjax + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /