diff --git a/include/config.h b/include/config.h index b96c17b8..6a6618bd 100644 --- a/include/config.h +++ b/include/config.h @@ -15,9 +15,9 @@ #include #include #include -#include #include "converter.h" +#include "fieldmanager.h" #include "events.h" #include "gridsettings.h" #include "scriptsettings.h" @@ -26,11 +26,6 @@ #include "orderedset.h" #include "block.h" -// TODO: Go through and re-test the default window geometries. -// TODO: Value validation? Make sure anything no longer validated can't cause problems. -// TODO: Documentation -// TODO: File splits - extern const QVersionNumber porymapVersion; // Distance in pixels from the edge of a GBA screen (240x160) to the center 16x16 pixels. @@ -58,12 +53,11 @@ public: virtual bool save(); // Loads the contents of the config from disk. - // The file to load from depends on the file name given to - // the constructor, and the root given to setRoot, if any. + // Returns true if saving was successful, false otherwise. // A successful load includes initializing an empty or non-existing file. virtual bool load(); - virtual QJsonObject toJson() const; + virtual QJsonObject toJson(); virtual void loadFromJson(const QJsonObject& obj); void setRoot(const QString& dir); @@ -74,31 +68,7 @@ protected: virtual void initializeFromEmpty() {}; virtual QJsonObject getDefaultJson() const { return QJsonObject(); } - struct FieldManager { - QJsonValue (*get)(const KeyValueConfigBase*); - QStringList (*set)(KeyValueConfigBase*, const QJsonValue&); - }; - template - static FieldManager makeFieldManager() { - // Deduce the type from the pointer to the member variable. - using T = std::remove_reference_t().*Member)>; - return FieldManager{ - .get = [](const KeyValueConfigBase* base) -> QJsonValue { - auto c = static_cast(base); - return Converter::toJson(c->*Member); - }, - .set = [](KeyValueConfigBase* base, const QJsonValue& json) { - QStringList errors; - const T value = Converter::fromJson(json, &errors); - if (errors.isEmpty()) static_cast(base)->*Member = value; - return errors; - } - }; - } - virtual const QHash& registeredFields() const { - static QHash empty; - return empty; - } + virtual FieldManager* getFieldManager() { return nullptr; } virtual bool parseJsonKeyValue(const QString& key, const QJsonValue& value); virtual bool parseLegacyKeyValue(const QString& , const QString& ) {return false;} @@ -109,7 +79,6 @@ protected: private: bool loadLegacy(); - // TODO: Make this setting accessible somewhere bool m_saveAllFields = true; }; @@ -194,86 +163,86 @@ public: // Since the release of the Retina display, Apple products use the Display P3 color space by default. // If we don't use this for exported images (which by default will either have no color space or the sRGB // color space) then they may appear to have different colors than the same image displayed in Porymap. - int imageExportColorSpaceId = static_cast(QColorSpace::DisplayP3); + std::optional imageExportColorSpace = QColorSpace::DisplayP3; #else // As of writing Qt has no way to get a reasonable color space from the user's environment, // so we export images without one and let them handle it. - int imageExportColorSpaceId = 0; + std::optional imageExportColorSpace = {}; #endif QMap trustedScriptHashes; -#define REGISTER(key, member) {QStringLiteral(key), makeFieldManager()} - const QHash& registeredFields() const override { - static const QHash fields = { - REGISTER("reopen_on_launch", reopenOnLaunch), - REGISTER("project_manually_closed", projectManuallyClosed), - REGISTER("map_list_tab", mapListTab), - REGISTER("map_list_edit_groups_enabled", mapListEditGroupsEnabled), - REGISTER("map_list_tabs_hiding_empty_folders", mapListTabsHidingEmptyFolders), - REGISTER("map_list_layouts_sorted", mapListLayoutsSorted), - REGISTER("map_list_locations_sorted", mapListLocationsSorted), - REGISTER("pretty_cursors", prettyCursors), - REGISTER("mirror_connecting_maps", mirrorConnectingMaps), - REGISTER("show_dive_emerge_maps", showDiveEmergeMaps), - REGISTER("dive_emerge_map_opacity", diveEmergeMapOpacity), - REGISTER("dive_map_opacity", diveMapOpacity), - REGISTER("emerge_map_opacity", emergeMapOpacity), - REGISTER("collision_opacity", collisionOpacity), - REGISTER("collision_zoom", collisionZoom), - REGISTER("metatiles_zoom", metatilesZoom), - REGISTER("tileset_editor_metatiles_zoom", tilesetEditorMetatilesZoom), - REGISTER("tileset_editor_tiles_zoom", tilesetEditorTilesZoom), - REGISTER("tileset_editor_layer_orientation", tilesetEditorLayerOrientation), - REGISTER("show_player_view", showPlayerView), - REGISTER("show_cursor_tile", showCursorTile), - REGISTER("show_border", showBorder), - REGISTER("show_grid", showGrid), - REGISTER("show_tileset_editor_metatile_grid", showTilesetEditorMetatileGrid), - REGISTER("show_tileset_editor_layer_grid", showTilesetEditorLayerGrid), - REGISTER("show_tileset_editor_divider", showTilesetEditorDivider), - REGISTER("show_tileset_editor_raw_attributes", showTilesetEditorRawAttributes), - REGISTER("show_palette_editor_unused_colors", showPaletteEditorUnusedColors), - REGISTER("monitor_files", monitorFiles), - REGISTER("tileset_checkerboard_fill", tilesetCheckerboardFill), - REGISTER("new_map_header_section_expanded", newMapHeaderSectionExpanded), - REGISTER("display_ids_hexadecimal", displayIdsHexadecimal), - REGISTER("theme", theme), - REGISTER("wild_mon_chart_theme", wildMonChartTheme), - REGISTER("text_editor_open_folder", textEditorOpenFolder), - REGISTER("text_editor_goto_line", textEditorGotoLine), - REGISTER("palette_editor_bit_depth", paletteEditorBitDepth), - REGISTER("project_settings_tab", projectSettingsTab), - REGISTER("script_autocomplete_mode", scriptAutocompleteMode), - REGISTER("warp_behavior_warning_disabled", warpBehaviorWarningDisabled), - REGISTER("event_delete_warning_disabled", eventDeleteWarningDisabled), - REGISTER("event_overlay_enabled", eventOverlayEnabled), - REGISTER("check_for_updates", checkForUpdates), - REGISTER("show_project_loading_screen", showProjectLoadingScreen), - REGISTER("last_update_check_time", lastUpdateCheckTime), - REGISTER("last_update_check_version", lastUpdateCheckVersion), - REGISTER("rate_limit_times", rateLimitTimes), - REGISTER("event_selection_shape_mode", eventSelectionShapeMode), - REGISTER("shown_in_game_reload_message", shownInGameReloadMessage), - REGISTER("map_grid", gridSettings), - REGISTER("status_bar_log_types", statusBarLogTypes), - REGISTER("application_font", applicationFont), - REGISTER("map_list_font", mapListFont), - REGISTER("image_export_color_space_id", imageExportColorSpaceId), - REGISTER("trusted_script_hashes", trustedScriptHashes), + FieldManager* getFieldManager() override { + if (!m_fm) { + m_fm = std::make_shared(); + m_fm->addField(&this->reopenOnLaunch, "reopen_on_launch"); + m_fm->addField(&this->projectManuallyClosed, "project_manually_closed"); + m_fm->addField(&this->mapListTab, "map_list_tab", 0, 2); + m_fm->addField(&this->mapListEditGroupsEnabled, "map_list_edit_groups_enabled"); + m_fm->addField(&this->mapListTabsHidingEmptyFolders, "map_list_tabs_hiding_empty_folders"); + m_fm->addField(&this->mapListLayoutsSorted, "map_list_layouts_sorted"); + m_fm->addField(&this->mapListLocationsSorted, "map_list_locations_sorted"); + m_fm->addField(&this->prettyCursors, "pretty_cursors"); + m_fm->addField(&this->mirrorConnectingMaps, "mirror_connecting_maps"); + m_fm->addField(&this->showDiveEmergeMaps, "show_dive_emerge_maps"); + m_fm->addField(&this->diveEmergeMapOpacity, "dive_emerge_map_opacity", 10, 90); + m_fm->addField(&this->diveMapOpacity, "dive_map_opacity", 10, 90); + m_fm->addField(&this->emergeMapOpacity, "emerge_map_opacity", 10, 90); + m_fm->addField(&this->collisionOpacity, "collision_opacity", 0, 100); + m_fm->addField(&this->collisionZoom, "collision_zoom", 10, 100); + m_fm->addField(&this->metatilesZoom, "metatiles_zoom", 10, 100); + m_fm->addField(&this->tilesetEditorMetatilesZoom, "tileset_editor_metatiles_zoom", 10, 100); + m_fm->addField(&this->tilesetEditorTilesZoom, "tileset_editor_tiles_zoom", 10, 100); + m_fm->addField(&this->tilesetEditorLayerOrientation, "tileset_editor_layer_orientation"); + m_fm->addField(&this->showPlayerView, "show_player_view"); + m_fm->addField(&this->showCursorTile, "show_cursor_tile"); + m_fm->addField(&this->showBorder, "show_border"); + m_fm->addField(&this->showGrid, "show_grid"); + m_fm->addField(&this->showTilesetEditorMetatileGrid, "show_tileset_editor_metatile_grid"); + m_fm->addField(&this->showTilesetEditorLayerGrid, "show_tileset_editor_layer_grid"); + m_fm->addField(&this->showTilesetEditorDivider, "show_tileset_editor_divider"); + m_fm->addField(&this->showTilesetEditorRawAttributes, "show_tileset_editor_raw_attributes"); + m_fm->addField(&this->showPaletteEditorUnusedColors, "show_palette_editor_unused_colors"); + m_fm->addField(&this->monitorFiles, "monitor_files"); + m_fm->addField(&this->tilesetCheckerboardFill, "tileset_checkerboard_fill"); + m_fm->addField(&this->newMapHeaderSectionExpanded, "new_map_header_section_expanded"); + m_fm->addField(&this->displayIdsHexadecimal, "display_ids_hexadecimal"); + m_fm->addField(&this->theme, "theme"); + m_fm->addField(&this->wildMonChartTheme, "wild_mon_chart_theme"); + m_fm->addField(&this->textEditorOpenFolder, "text_editor_open_folder"); + m_fm->addField(&this->textEditorGotoLine, "text_editor_goto_line"); + m_fm->addField(&this->paletteEditorBitDepth, "palette_editor_bit_depth", {24,15}); + m_fm->addField(&this->projectSettingsTab, "project_settings_tab"); + m_fm->addField(&this->scriptAutocompleteMode, "script_autocomplete_mode"); + m_fm->addField(&this->warpBehaviorWarningDisabled, "warp_behavior_warning_disabled"); + m_fm->addField(&this->eventDeleteWarningDisabled, "event_delete_warning_disabled"); + m_fm->addField(&this->eventOverlayEnabled, "event_overlay_enabled"); + m_fm->addField(&this->checkForUpdates, "check_for_updates"); + m_fm->addField(&this->showProjectLoadingScreen, "show_project_loading_screen"); + m_fm->addField(&this->lastUpdateCheckTime, "last_update_check_time"); + m_fm->addField(&this->lastUpdateCheckVersion, "last_update_check_version"); + m_fm->addField(&this->rateLimitTimes, "rate_limit_times"); + m_fm->addField(&this->eventSelectionShapeMode, "event_selection_shape_mode"); + m_fm->addField(&this->shownInGameReloadMessage, "shown_in_game_reload_message"); + m_fm->addField(&this->gridSettings, "map_grid"); + m_fm->addField(&this->statusBarLogTypes, "status_bar_log_types"); + m_fm->addField(&this->applicationFont, "application_font"); + m_fm->addField(&this->mapListFont, "map_list_font"); + m_fm->addField(&this->imageExportColorSpace, "image_export_color_space"); + m_fm->addField(&this->trustedScriptHashes, "trusted_script_hashes"); - REGISTER("recent_projects", recentProjects), - REGISTER("geometry", savedGeometryMap), - REGISTER("geometry_version", geometryVersion), - }; - return fields; + m_fm->addField(&this->recentProjects, "recent_projects"); + m_fm->addField(&this->savedGeometryMap, "geometry"); + m_fm->addField(&this->geometryVersion, "geometry_version"); + } + return m_fm.get(); }; -#undef REGISTER protected: virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; virtual QJsonObject getDefaultJson() const override; private: + std::shared_ptr m_fm = nullptr; QStringList recentProjects; QMap savedGeometryMap; int geometryVersion = 0; @@ -470,64 +439,63 @@ public: QMap pokemonIconPaths; QVersionNumber minimumVersion; -#define REGISTER(key, member) {QStringLiteral(key), makeFieldManager()} - const QHash& registeredFields() const override { - static const QHash fields = { - REGISTER("base_game_version", baseGameVersion), - REGISTER("use_poryscript", usePoryScript), - REGISTER("use_custom_border_size", useCustomBorderSize), - REGISTER("enable_event_weather_trigger", eventWeatherTriggerEnabled), - REGISTER("enable_event_secret_base", eventSecretBaseEnabled), - REGISTER("enable_hidden_item_quantity", hiddenItemQuantityEnabled), - REGISTER("enable_hidden_item_requires_itemfinder", hiddenItemRequiresItemfinderEnabled), - REGISTER("enable_heal_location_respawn_data", healLocationRespawnDataEnabled), - REGISTER("enable_event_clone_object", eventCloneObjectEnabled), - REGISTER("enable_floor_number", floorNumberEnabled), - REGISTER("create_map_text_file", createMapTextFileEnabled), - REGISTER("enable_triple_layer_metatiles", tripleLayerMetatilesEnabled), - REGISTER("default_metatile_id", defaultMetatileId), - REGISTER("default_elevation", defaultElevation), - REGISTER("default_collision", defaultCollision), - REGISTER("default_map_size", defaultMapSize), - REGISTER("new_map_border_metatiles", newMapBorderMetatileIds), - REGISTER("default_primary_tileset", defaultPrimaryTileset), - REGISTER("default_secondary_tileset", defaultSecondaryTileset), - REGISTER("tilesets_have_callback", tilesetsHaveCallback), - REGISTER("tilesets_have_is_compressed", tilesetsHaveIsCompressed), - REGISTER("transparency_color", transparencyColor), - REGISTER("preserve_matching_only_data", preserveMatchingOnlyData), - REGISTER("metatile_attributes_size", metatileAttributesSize), - REGISTER("metatile_behavior_mask", metatileBehaviorMask), - REGISTER("metatile_terrain_type_mask", metatileTerrainTypeMask), - REGISTER("metatile_encounter_type_mask", metatileEncounterTypeMask), - REGISTER("metatile_layer_type_mask", metatileLayerTypeMask), - REGISTER("block_metatile_id_mask", blockMetatileIdMask), - REGISTER("block_collision_mask", blockCollisionMask), - REGISTER("block_elevation_mask", blockElevationMask), - REGISTER("unused_tile_normal", unusedTileNormal), - REGISTER("unused_tile_covered", unusedTileCovered), - REGISTER("unused_tile_split", unusedTileSplit), - REGISTER("enable_map_allow_flags", mapAllowFlagsEnabled), - REGISTER("events_tab_icon_path", eventsTabIconPath), - REGISTER("collision_sheet_path", collisionSheetPath), - REGISTER("collision_sheet_size", collisionSheetSize), - REGISTER("player_view_distance", playerViewDistance), - REGISTER("warp_behaviors", warpBehaviors), - REGISTER("max_events_per_group", maxEventsPerGroup), - REGISTER("metatile_selector_width", metatileSelectorWidth), - REGISTER("global_constants_filepaths", globalConstantsFilepaths), - REGISTER("global_constants", globalConstants), - REGISTER("custom_scripts", customScripts), - REGISTER("event_icon_paths", eventIconPaths), - REGISTER("pokemon_icon_paths", pokemonIconPaths), - REGISTER("minimum_version", minimumVersion), + FieldManager* getFieldManager() override { + if (!m_fm) { + m_fm = std::make_shared(); + m_fm->addField(&this->baseGameVersion, "base_game_version"); + m_fm->addField(&this->usePoryScript, "use_poryscript"); + m_fm->addField(&this->useCustomBorderSize, "use_custom_border_size"); + m_fm->addField(&this->eventWeatherTriggerEnabled, "enable_event_weather_trigger"); + m_fm->addField(&this->eventSecretBaseEnabled, "enable_event_secret_base"); + m_fm->addField(&this->hiddenItemQuantityEnabled, "enable_hidden_item_quantity"); + m_fm->addField(&this->hiddenItemRequiresItemfinderEnabled, "enable_hidden_item_requires_itemfinder"); + m_fm->addField(&this->healLocationRespawnDataEnabled, "enable_heal_location_respawn_data"); + m_fm->addField(&this->eventCloneObjectEnabled, "enable_event_clone_object"); + m_fm->addField(&this->floorNumberEnabled, "enable_floor_number"); + m_fm->addField(&this->createMapTextFileEnabled, "create_map_text_file"); + m_fm->addField(&this->tripleLayerMetatilesEnabled, "enable_triple_layer_metatiles"); + m_fm->addField(&this->defaultMetatileId, "default_metatile_id", 0, Block::MaxValue); + m_fm->addField(&this->defaultElevation, "default_elevation", 0, Block::MaxValue); + m_fm->addField(&this->defaultCollision, "default_collision", 0, Block::MaxValue); + m_fm->addField(&this->defaultMapSize, "default_map_size"); + m_fm->addField(&this->newMapBorderMetatileIds, "new_map_border_metatiles"); + m_fm->addField(&this->defaultPrimaryTileset, "default_primary_tileset"); + m_fm->addField(&this->defaultSecondaryTileset, "default_secondary_tileset"); + m_fm->addField(&this->tilesetsHaveCallback, "tilesets_have_callback"); + m_fm->addField(&this->tilesetsHaveIsCompressed, "tilesets_have_is_compressed"); + m_fm->addField(&this->transparencyColor, "transparency_color"); + m_fm->addField(&this->preserveMatchingOnlyData, "preserve_matching_only_data"); + m_fm->addField(&this->metatileAttributesSize, "metatile_attributes_size"); + m_fm->addField(&this->metatileBehaviorMask, "metatile_behavior_mask"); + m_fm->addField(&this->metatileTerrainTypeMask, "metatile_terrain_type_mask"); + m_fm->addField(&this->metatileEncounterTypeMask, "metatile_encounter_type_mask"); + m_fm->addField(&this->metatileLayerTypeMask, "metatile_layer_type_mask"); + m_fm->addField(&this->blockMetatileIdMask, "block_metatile_id_mask", 0, Block::MaxValue); + m_fm->addField(&this->blockCollisionMask, "block_collision_mask", 0, Block::MaxValue); + m_fm->addField(&this->blockElevationMask, "block_elevation_mask", 0, Block::MaxValue); + m_fm->addField(&this->unusedTileNormal, "unused_tile_normal", 0, Tile::MaxValue); + m_fm->addField(&this->unusedTileCovered, "unused_tile_covered", 0, Tile::MaxValue); + m_fm->addField(&this->unusedTileSplit, "unused_tile_split", 0, Tile::MaxValue); + m_fm->addField(&this->mapAllowFlagsEnabled, "enable_map_allow_flags"); + m_fm->addField(&this->eventsTabIconPath, "events_tab_icon_path"); + m_fm->addField(&this->collisionSheetPath, "collision_sheet_path"); + m_fm->addField(&this->collisionSheetSize, "collision_sheet_size", QSize(1,1), QSize(Block::MaxValue, Block::MaxValue)); + m_fm->addField(&this->playerViewDistance, "player_view_distance", QMargins(0,0,0,0), QMargins(INT_MAX, INT_MAX, INT_MAX, INT_MAX)); + m_fm->addField(&this->warpBehaviors, "warp_behaviors"); + m_fm->addField(&this->maxEventsPerGroup, "max_events_per_group", 1, INT_MAX); + m_fm->addField(&this->metatileSelectorWidth, "metatile_selector_width", 1, INT_MAX); + m_fm->addField(&this->globalConstantsFilepaths, "global_constants_filepaths"); + m_fm->addField(&this->globalConstants, "global_constants"); + m_fm->addField(&this->customScripts, "custom_scripts"); + m_fm->addField(&this->eventIconPaths, "event_icon_paths"); + m_fm->addField(&this->pokemonIconPaths, "pokemon_icon_paths"); + m_fm->addField(&this->minimumVersion, "minimum_version"); - REGISTER("custom_identifiers", identifiers), - REGISTER("custom_file_paths", filePaths), - }; - return fields; + m_fm->addField(&this->identifiers, "custom_identifiers"); + m_fm->addField(&this->filePaths, "custom_file_paths"); + } + return m_fm.get(); } -#undef REGISTER protected: virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; @@ -538,6 +506,7 @@ private: ProjectFilePath reverseDefaultPaths(const QString& str); ProjectIdentifier reverseDefaultIdentifier(const QString& str); + std::shared_ptr m_fm = nullptr; QMap identifiers; QMap filePaths; }; @@ -565,18 +534,20 @@ protected: virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; virtual QJsonObject getDefaultJson() const override; -#define REGISTER(key, member) {QStringLiteral(key), makeFieldManager()} - const QHash& registeredFields() const override { - static const QHash fields = { - REGISTER("recent_map_or_layout", recentMapOrLayout), - REGISTER("prefabs_filepath", prefabsFilepath), - REGISTER("prefabs_import_prompted", prefabsImportPrompted), - REGISTER("use_encounter_json", useEncounterJson), - REGISTER("custom_scripts", customScripts), - }; - return fields; + FieldManager* getFieldManager() override { + if (!m_fm) { + m_fm = std::make_shared(); + m_fm->addField(&this->recentMapOrLayout, "recent_map_or_layout"); + m_fm->addField(&this->prefabsFilepath, "prefabs_filepath"); + m_fm->addField(&this->prefabsImportPrompted, "prefabs_import_prompted"); + m_fm->addField(&this->useEncounterJson, "use_encounter_json"); + m_fm->addField(&this->customScripts, "custom_scripts"); + } + return m_fm.get(); } -#undef REGISTER + +private: + std::shared_ptr m_fm = nullptr; }; extern UserConfig userConfig; @@ -591,7 +562,7 @@ public: setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); } - virtual QJsonObject toJson() const override; + virtual QJsonObject toJson() override; virtual void loadFromJson(const QJsonObject& obj) override; // Call this before applying user shortcuts so that the user can restore defaults. diff --git a/include/core/converter.h b/include/core/converter.h index a193386f..6ddf92fc 100644 --- a/include/core/converter.h +++ b/include/core/converter.h @@ -47,6 +47,8 @@ Appropriately implementing 'toString'/'fromString' has the added benefit that your type can automatically be used as a JSON key if it for example appears as the key in a QMap. + // TODO: Document clamp + */ @@ -69,6 +71,19 @@ struct DefaultConverter { // Default to identity static QString toString(const T& value) {return value;} static T fromString(const QString& string, QStringList* = nullptr) {return string;} + + static T clamp(const T& value, const T& min, const T& max, QStringList* errors = nullptr) { + Q_ASSERT(min <= max); + if (value < min) { + if (errors) errors->append("Value too low"); + return min; + } + if (value > max) { + if (errors) errors->append("Value too high"); + return max; + } + return value; + } }; template @@ -189,6 +204,17 @@ struct Converter : DefaultStringConverter } }; +template +struct Converter> : DefaultConverter> { + static QJsonValue toJson(const std::optional& optional) { + return optional.has_value() ? Converter::toJson(optional.value()) : QJsonValue(); + } + static std::optional fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + if (json.isNull()) return {}; + return Converter::fromJson(json, errors); + } +}; + template struct Converter> : DefaultConverter> { static QJsonValue toJson(const QList& list) { @@ -277,6 +303,14 @@ struct Converter : DefaultConverter { size.setHeight(obj.value("height").toInt()); return size; } + static QSize clamp(const QSize& value, const QSize& min, const QSize& max, const QStringList* = nullptr) { + Q_ASSERT(min.width() <= max.width()); + Q_ASSERT(min.height() <= max.height()); + QSize size = value; + if (value.width() < min.width() || value.height() < min.height()) size = value.expandedTo(min); + if (value.width() > max.width() || value.height() > max.height()) size = value.boundedTo(max); + return size; + } }; template <> @@ -298,6 +332,18 @@ struct Converter : DefaultConverter { margins.setRight(obj.value("right").toInt()); return margins; } + static QMargins clamp(const QMargins& value, const QMargins& min, const QMargins& max, const QStringList* = nullptr) { + Q_ASSERT(min.left() <= max.left()); + Q_ASSERT(min.right() <= max.right()); + Q_ASSERT(min.top() <= max.top()); + Q_ASSERT(min.bottom() <= max.bottom()); + QMargins margins = value; + margins.setLeft( std::clamp(value.left(), min.left(), max.left())); + margins.setRight( std::clamp(value.right(), min.right(), max.right())); + margins.setTop( std::clamp(value.top(), min.top(), max.top())); + margins.setBottom(std::clamp(value.bottom(), min.bottom(), max.bottom())); + return margins; + } }; template <> diff --git a/include/core/fieldmanager.h b/include/core/fieldmanager.h new file mode 100644 index 00000000..20005701 --- /dev/null +++ b/include/core/fieldmanager.h @@ -0,0 +1,171 @@ +#include "converter.h" + + +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +// A FieldInterface provides a simple interface for converting a field to/from JSON. +// It's constructed with a pointer to some data, and has two functions: +// - 'get' returns the pointed-to data, converted to JSON +// - 'set' assigns the pointed-to data to a given QJsonValue, with appropriate conversion. +// Returns any errors that occur during conversion/assignment. +// +// A FieldInterface is normally constructed using the 'makeFieldInterface' function: +// Example: +// int someField = 0; +// FieldInterface* fi = makeFieldInterface(&someField); +// fi->set(QJsonValue("5")); // someField is now 5 +// +// There are additional implementations of 'makeFieldInterface' that let you specify what valid values are. +// Example: +// int someField = 0; +// int min = 1, max = 4; +// FieldInterface* fi = makeFieldInterface(&someField, min, max); +// fi->set(QJsonValue("5")); // someField is now 4 (defaults to closest bound), error messages returned +// or +// QString someField = ""; +// QList options = {"hello","hi there"}; +// FieldInterface* fi = makeFieldInterface(&someField, options); +// fi->set(QJsonValue("5")); // someField is now "hello" (defaults to first element), error messages returned + + +// Base class lets us use the interface without any type information. +class FieldInterface { +public: + FieldInterface(){}; + virtual ~FieldInterface() {}; + virtual QJsonValue get() const = 0; + virtual QStringList set(const QJsonValue& json) const = 0; +}; + +template +class BasicFieldInterface : public FieldInterface { +public: + BasicFieldInterface(T* field) : m_field(field) { + Q_ASSERT(m_field); + }; + virtual ~BasicFieldInterface() {}; + virtual QJsonValue get() const override {return Converter::toJson(*m_field);} + virtual QStringList set(const QJsonValue& json) const override { + QStringList errors; + auto value = Converter::fromJson(json, &errors); + if (errors.isEmpty()) *m_field = value; // Don't bother changing the value if conversion failed + return errors; + } +protected: + T* m_field; +}; + +template +static FieldInterface* makeFieldInterface(T* field) { + return new BasicFieldInterface(field); +} + +// Create a regular FieldInterface, but override 'set' to use the given min/max. +template +static FieldInterface* makeFieldInterface(T* field, const T& min, const T& max) { + class BoundedFieldInterface : public BasicFieldInterface { + public: + BoundedFieldInterface(T* field, const T& min, const T& max) + : BasicFieldInterface(field), m_min(min), m_max(max) {}; + virtual ~BoundedFieldInterface() {}; + virtual QStringList set(const QJsonValue& json) const override { + QStringList errors; + auto value = Converter::fromJson(json, &errors); + if (errors.isEmpty()) { // Don't bother changing the value if conversion failed + value = Converter::clamp(value, m_min, m_max, &errors); + *this->m_field = value; + } + return errors; + } + private: + const T m_min; + const T m_max; + }; + return new BoundedFieldInterface(field, min, max); +} + +// Create a regular FieldInterface, but override 'set' to use the given 'acceptableValues'. +template +static FieldInterface* makeFieldInterface(T* field, const QList& acceptableValues) { + Q_ASSERT(!acceptableValues.isEmpty()); + class BoundedFieldInterface : public BasicFieldInterface { + public: + BoundedFieldInterface(T* field, const QList& acceptableValues) + : BasicFieldInterface(field), + m_acceptableValues(acceptableValues.begin(), acceptableValues.end()), + m_defaultValue(acceptableValues.first()) {}; + virtual ~BoundedFieldInterface() {}; + virtual QStringList set(const QJsonValue& json) const override { + QStringList errors; + auto value = Converter::fromJson(json, &errors); + if (errors.isEmpty()) { + if (!m_acceptableValues.contains(value)) { + value = m_defaultValue; + errors.append("Invalid value."); + } + *this->m_field = value; + } + return errors; + } + private: + // The order of the list only matters for determining the default value, + // so save that separately and convert the list to a set for better lookup speed. + const QSet m_acceptableValues; + const T m_defaultValue; + }; + return new BoundedFieldInterface(field, acceptableValues); +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +// FieldManager manages a QHash mapping string keys to FieldInterfaces. +// This makes it easy to map many fields to/from JSON without explicitly serializing anything. + +class FieldManager { +public: + ~FieldManager(){ clear(); } + + QStringList setField(const QString& key, const QJsonValue& value) const { + auto it = m_fields.find(key); + return (it != m_fields.end()) ? it.value()->set(value) : QStringList(); + } + + QJsonValue getField(const QString& key) const { + auto it = m_fields.find(key); + return (it != m_fields.end()) ? it.value()->get() : QJsonValue(); + } + + QJsonObject getFields() const { + QJsonObject obj; + for (auto it = m_fields.constBegin(); it != m_fields.constEnd(); it++) { + obj[it.key()] = it.value()->get(); + } + return obj; + } + + void clear() { + qDeleteAll(m_fields); + m_fields.clear(); + } + + bool hasField(const QString& key) const {return m_fields.contains(key);} + + template + void addField(T* field, const QString& key) { + Q_ASSERT(!m_fields.contains(key)); + m_fields.insert(key, makeFieldInterface(field)); + } + template + void addField(T* field, const QString& key, const T& min, const T& max) { + Q_ASSERT(!m_fields.contains(key)); + m_fields.insert(key, makeFieldInterface(field, min, max)); + } + template + void addField(T* field, const QString& key, const QList& acceptableValues) { + Q_ASSERT(!m_fields.contains(key)); + m_fields.insert(key, makeFieldInterface(field, acceptableValues)); + } + +private: + QHash m_fields; +}; diff --git a/include/core/orderedset.h b/include/core/orderedset.h index 717c94f4..8f5bb185 100644 --- a/include/core/orderedset.h +++ b/include/core/orderedset.h @@ -3,6 +3,7 @@ #define ORDERED_SET_H #include +#include template class OrderedSet : public std::set diff --git a/include/core/utility.h b/include/core/utility.h index 54e53305..40dd1423 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -18,7 +18,6 @@ namespace Util { void setErrorStylesheet(QLineEdit *lineEdit, bool isError); QString toStylesheetString(const QFont &font); void show(QWidget *w); - QColorSpace toColorSpace(int colorSpaceInt); QString mkpath(const QString& dirPath); QString getFileHash(const QString &filepath); diff --git a/src/config.cpp b/src/config.cpp index b55d1b8f..0321c7e2 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -199,24 +199,19 @@ void KeyValueConfigBase::loadFromJson(const QJsonObject& obj) { } bool KeyValueConfigBase::parseJsonKeyValue(const QString& key, const QJsonValue& value) { - const auto fields = registeredFields(); - auto it = fields.find(key); - if (it == fields.end()) return false; + auto fieldManager = getFieldManager(); + if (!fieldManager || !fieldManager->hasField(key)) return false; // Recognized 'key' as a registered field. Let the FieldManager try to assign the value. - const QStringList errors = it.value().set(this, value); + const QStringList errors = fieldManager->setField(key, value); if (errors.length() == 1) logWarn(QString("Failed to read config key '%1': %2").arg(key).arg(errors.at(0))); else if (errors.length() > 1) logWarn(QString("Failed to read config key '%1':\n%2").arg(key).arg(errors.join("\n"))); return true; } -QJsonObject KeyValueConfigBase::toJson() const { - QJsonObject obj; - const auto fields = registeredFields(); - for (auto it = fields.begin(); it != fields.end(); it++) { - obj[it.key()] = it.value().get(this); - } - return obj; +QJsonObject KeyValueConfigBase::toJson() { + auto fieldManager = getFieldManager(); + return fieldManager ? fieldManager->getFields() : QJsonObject(); } bool KeyValueConfigBase::save() { @@ -279,6 +274,9 @@ void PorymapConfig::loadFromJson(const QJsonObject& obj) { this->geometryVersion = CurrentGeometryVersion; this->savedGeometryMap.clear(); } + + this->gridSettings.offsetX = std::clamp(this->gridSettings.offsetX, 0, 999); + this->gridSettings.offsetY = std::clamp(this->gridSettings.offsetY, 0, 999); } QJsonObject PorymapConfig::getDefaultJson() const { @@ -606,7 +604,7 @@ void ShortcutsConfig::loadFromJson(const QJsonObject& obj) { this->user_shortcuts = Converter>::fromJson(obj); } -QJsonObject ShortcutsConfig::toJson() const { +QJsonObject ShortcutsConfig::toJson() { return Converter>::toJson(this->user_shortcuts); } diff --git a/src/config/legacy.cpp b/src/config/legacy.cpp index 810a93b7..e28776de 100644 --- a/src/config/legacy.cpp +++ b/src/config/legacy.cpp @@ -112,7 +112,6 @@ bool PorymapConfig::parseLegacyKeyValue(const QString &key, const QString &value this->emergeMapOpacity = toInt(value, 10, 90, 15); } else if (key == "collision_opacity") { this->collisionOpacity = toInt(value, 0, 100, 50); - } else if (key == "tileset_editor_geometry") { } else if (key == "metatiles_zoom") { this->metatilesZoom = toInt(value, 10, 100, 30); } else if (key == "collision_zoom") { @@ -225,8 +224,6 @@ bool PorymapConfig::parseLegacyKeyValue(const QString &key, const QString &value } else if (key == "map_list_font") { this->mapListFont = QFont(); this->mapListFont.fromString(value); - } else if (key == "image_export_color_space_id") { - this->imageExportColorSpaceId = toInt(value, 0, 8); } else { return false; } diff --git a/src/core/utility.cpp b/src/core/utility.cpp index 77cf8e3e..b6434c4a 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -129,26 +129,6 @@ void Util::show(QWidget *w) { } } -// Safe conversion from an int representing a QColorSpace::NamedColorSpace to a QColorSpace. -// This lets us use 0 to mean "no color space". -QColorSpace Util::toColorSpace(int colorSpaceInt) { - QColorSpace colorSpace; - - int min = static_cast(QColorSpace::SRgb); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)) - // Qt 6.8.0 introduced additional color spaces - int max = static_cast(QColorSpace::Bt2100Hlg); -#else - int max = static_cast(QColorSpace::ProPhotoRgb); -#endif - - if (colorSpaceInt >= min && colorSpaceInt <= max) { - return QColorSpace(static_cast(colorSpaceInt)); - } else { - return QColorSpace(); - } -} - // Creates a directory named 'dirPath', including any non-existent parent directories. Returns an error message, if any. // If 'dirPath' already exists it's considered an error unless the directory has no files. QString Util::mkpath(const QString& dirPath) { diff --git a/src/project.cpp b/src/project.cpp index 1beeea3b..1ac82a98 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -82,7 +82,7 @@ QVersionNumber Project::getMinimumVersion(QString *errorOut) const { // No explicitly supported version, we can use the project's git history (if it has one, and we're able to get it) to make a reasonable guess. // We know the hashes of the commits in the base repos that contain breaking changes, so if we find one of these then the project - // should support at least up to that Porymap major version. If this fails for any reason it returns a version of -1. + // should support at least up to that Porymap major version. If this fails for any reason it returns an empty QVersionNumber. // This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check // is not important enough to significantly slow down project launch, we'd rather just timeout. diff --git a/src/ui/customscriptslistitem.cpp b/src/ui/customscriptslistitem.cpp index 17482fe7..ab00ef0b 100644 --- a/src/ui/customscriptslistitem.cpp +++ b/src/ui/customscriptslistitem.cpp @@ -40,16 +40,16 @@ bool CustomScriptsListItem::scriptEnabled() const { return ui->checkBox_Enable->isChecked(); } +// TODO: The two functions below should read/write from/to the UI whether the script belongs to the project or user. void CustomScriptsListItem::setSettings(const ScriptSettings& settings) { setPath(settings.path); setScriptEnabled(settings.enabled); - // TODO: Read userOnly } ScriptSettings CustomScriptsListItem::getSettings() const { return { .path = path(), .enabled = scriptEnabled(), - .userOnly = true, // TODO + .userOnly = true, }; } diff --git a/src/ui/eventfilters.cpp b/src/ui/eventfilters.cpp index 1cda13b8..df3e65c4 100644 --- a/src/ui/eventfilters.cpp +++ b/src/ui/eventfilters.cpp @@ -54,4 +54,4 @@ bool GeometrySaver::eventFilter(QObject *object, QEvent *event) { } } return false; -} \ No newline at end of file +} diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index ab44566e..e507c50e 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -565,7 +565,9 @@ void MapImageExporter::updatePreview(bool forceUpdate) { } progress.close(); - m_previewImage.setColorSpace(Util::toColorSpace(porymapConfig.imageExportColorSpaceId)); + if (porymapConfig.imageExportColorSpace) { + m_previewImage.setColorSpace(QColorSpace(porymapConfig.imageExportColorSpace.value())); + } m_preview->setPixmap(QPixmap::fromImage(m_previewImage)); m_scene->setSceneRect(m_scene->itemsBoundingRect()); scalePreview(); diff --git a/src/ui/metatileimageexporter.cpp b/src/ui/metatileimageexporter.cpp index f7e99fa0..8261dad5 100644 --- a/src/ui/metatileimageexporter.cpp +++ b/src/ui/metatileimageexporter.cpp @@ -276,7 +276,9 @@ void MetatileImageExporter::updatePreview() { m_layerOrder); } - m_previewImage.setColorSpace(Util::toColorSpace(porymapConfig.imageExportColorSpaceId)); + if (porymapConfig.imageExportColorSpace) { + m_previewImage.setColorSpace(QColorSpace(porymapConfig.imageExportColorSpace.value())); + } m_preview->setPixmap(QPixmap::fromImage(m_previewImage)); m_scene->setSceneRect(m_scene->itemsBoundingRect()); m_previewUpdateQueued = false; diff --git a/src/ui/preferenceeditor.cpp b/src/ui/preferenceeditor.cpp index 5cb9c731..638fde9b 100644 --- a/src/ui/preferenceeditor.cpp +++ b/src/ui/preferenceeditor.cpp @@ -81,7 +81,11 @@ void PreferenceEditor::updateFields() { } else if (porymapConfig.eventSelectionShapeMode == QGraphicsPixmapItem::BoundingRectShape) { ui->radioButton_WithinRect->setChecked(true); } - ui->comboBox_ColorSpace->setNumberItem(porymapConfig.imageExportColorSpaceId); + if (porymapConfig.imageExportColorSpace) { + ui->comboBox_ColorSpace->setNumberItem(porymapConfig.imageExportColorSpace.value()); + } else { + ui->comboBox_ColorSpace->setNumberItem(0); + } ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.textEditorOpenFolder); ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.textEditorGotoLine); ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.monitorFiles); @@ -133,7 +137,12 @@ void PreferenceEditor::saveFields() { emit scriptSettingsChanged(scriptAutocompleteMode); } - porymapConfig.imageExportColorSpaceId = ui->comboBox_ColorSpace->currentData().toInt(); + auto colorSpace = magic_enum::enum_cast(ui->comboBox_ColorSpace->currentData().toInt()); + if (colorSpace.has_value()) { + porymapConfig.imageExportColorSpace = colorSpace.value(); + } else { + porymapConfig.imageExportColorSpace.reset(); + } porymapConfig.eventSelectionShapeMode = ui->radioButton_OnSprite->isChecked() ? QGraphicsPixmapItem::MaskShape : QGraphicsPixmapItem::BoundingRectShape; porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text(); porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text();