Addon presets - #1951
Conversation
Flamefire
left a comment
There was a problem hiding this comment.
Looks good, thanks!
Just some questions
- Replace ad-hoc checks in iwSave/iwAddonPresets with isValidFileName()/isValidFileNameChar() from libutil. - Add fileNameOnly_ mode to ctrlEdit filtering invalid chars on both AddChar() and SetText(). - Fix double-space bug: remove KeyType::Space from Msg_KeyDown, space already arrives via char/text-input event. - Set maxLength=251 on filename edits (255-byte limit minus 4-char extension). - Skip makePortableFileName() sanitization for ServerType::Local+MapType::Savegame to prevent renamed duplicates. - Migrate iwAddonPresets Msg_TableSelectItem from boost::optional to std::optional. - Add unit and integration tests for all new behaviours.
|
Last push contained:
|
Flamefire
left a comment
There was a problem hiding this comment.
Ok, thanks. Most comments are pretty much about the quality of the AI code. I tried to provide enough reasoning so you can catch that for future PRs too
- Replace `numberOnly_`/`fileNameOnly_` booleans with `EditType` enum and `SetType()` - Add `ctrlEdit::GetFileName(ext)` - trims, appends extension, validates, returns `GetFileNameResult` - Remove duplicated `GetSaveFilePath()` from `iwSave`/`iwAddonPresets`; use `GetFileName()` instead - Distinct error messages for empty vs. invalid filename in `iwSave`/`iwAddonPresets` - Update `dskOptions`/`iwTrade` callers to `SetType(EditType::Number)` - Move filename unit tests to `testControls.cpp`; add `AddonPresetSaveLoadAndOverwrite` integration test
- Close preset window together with iwAddons - Don't throw from preset window ctor if folder creation fails - Expose msgbox IDs as class constants, use in test - Free leaked overwrite msgbox in test - Reword test comments
|
Last few commits contained:
|
Flamefire
left a comment
There was a problem hiding this comment.
Small polishing only. I would have used a std::optional but I guess GetFileNameResult is more explicit, so good idea there!
BTW: Please don't resolve conversations during a review. I usually use them to check what I reviewed before and how that was implemented. If you wan't it to keep track for yourself you could e.g. add a thumbs-up reaction for the done-comments.
Exceptions are where the comment is a simple suggestion with not further text which you applied as-is. GitHub auto-resolves those anyway when using the UI.
- Name edit, not table selection, is the action target - GetSelectedFilePath -> GetTargetFilePath/GetTargetFileOrNotify - Fail-closed if presets folder can't be created - ctrlEdit::GetFileName always appends ext - ListDir: guard on is_directory() - New fixture + tests for the above
|
@Flamefire I think I addressed all of your recent comments except for the addons window modal/non-modal in which I await your reply. Last commits contained:
|
|
Corrected the clang-format and GCC issues that my MSVC build didn't catch. I've set up a local Linux dev env with clang-format, clang-tidy, and the gcc build at the same versions CI uses, so I should be able to catch these kinds of errors before pushing in the future. |
There are static clang-tools binaries also for Windows if you work rather there: https://github.com/muttleyxd/clang-tools-static-binaries/releases
@Spikeone I'd say it makes sense that the last opened modal-window will be the top window as I'd say this is the most intuitive. Lua-msg-windows (mission statements) pause the game so I don't really see where this would cause an issue there. Could there even be a situation with multiple statements in a GF? Do we have any? Would it be an issue if they get reversed, i.e. newest/last-added first? |
I agree that this is what I'd expect. |
- WindowManager: new modal goes on top, non-modal behind all modals - iwAddonPresets, iwAddons: construct as modal windows - GetTargetFilePath: match preset by table's displayed name - GetTargetFileOrNotify: now const - iwSaveAddonPreset: overwrite prompt names the preset - iwAddons: Show instead of ReplaceWindow for preset windows - ctrlEdit::GetFileName: only trim trailing space when no ext given - ListDir: revert to exists() check, drop is_directory() requirement - iwSave::SaveLoad: switch on FileNameStatus - Tests for modal stacking, mouse blocking, and preset matching
|
My latest commit includes:
|
|
@Flamefire I think all the comments are addressed. Any chance for a review? |
Flamefire
left a comment
There was a problem hiding this comment.
Ok thanks!
Just 2 more changes, one optional to not introduce another instance of addonGuis_[i]->setStatus(*GetCtrl<ctrlGroup>(ID_grpAddonsStart + i), status):
the assumption there is that the same group/window is always passed as used in the constructor. We should instead store a reference to it and not pass it again. Also replace the existing usages too.
I'd like to see the Get[Target]FilePath(name) change to avoid the hidden dependency on GetCtrl<ctrlEdit>(ID_edtName) and make it clear why clearing that edit afterwards makes sense. Or what do you think?
| bfs::path path = GetTargetFilePath(); | ||
| if(!path.empty()) | ||
| return path; | ||
|
|
||
| const std::string name = GetCtrl<ctrlEdit>(ID_edtName)->GetText(); | ||
| if(name.empty()) | ||
| return {}; |
There was a problem hiding this comment.
This is confusing. I had to check multiple times in which situation name and/or path can be empty. I think it is better to pass the name to GetTargetFilePath()
The only other usage is the deletion in the msgbox result where you then do GetCtrl<ctrlEdit>(ID_edtName)->SetText("")
If you get the text from there first that line makes much more sense.
I'd also assert that only non-empty names are passed to GetTargetFilePath so an empty result is always: name not found in table.
I guess GetFilePath(name) is then enough: "Convert" a name to a path using the table.
| const auto it = states.find(static_cast<unsigned>(addon->getId())); | ||
| const unsigned rawStatus = (it != states.end()) ? it->second : addon->getDefaultStatus(); | ||
| const unsigned status = (rawStatus < addon->getNumOptions()) ? rawStatus : addon->getDefaultStatus(); | ||
| addonGuis_[i]->setStatus(*GetCtrl<ctrlGroup>(ID_grpAddonsStart + i), status); |
There was a problem hiding this comment.
This looks odd, although it already exists...
Can you do a small cleanup commit and store the window/group in AddonGui and remove it from the set/getStatus functions?
Avoids the error-prone use of ID_grpAddonsStart +i in multiple cases. We'd need a getWindow (or so) function for UpdateView then.
Summary
Implements addon preset save/load functionality, as requested in issue #665.
Closes #665
Players can now save their current addon configuration to a named
.inifile and reload it later, instead of manually re-configuring addons each time.Changes
iwAddonPresets.h/.cpp- Two new dialog windows (iwSaveAddonPreset,iwLoadAddonPreset) built on a shared base class (iwAddonPresetsBase). The dialogs show a sortable list of saved presets, a name edit field, and buttons for the primary action and deletion (with confirmation). Double-clicking a row triggers the action directly.iwAddons- Added Save and Load buttons in a new button row above the existing Apply/Abort row. Window height increased from 500 to 530 px to accommodate the extra row. NewapplyAddonStates()method applies a loaded preset back into the GUI, respecting read-only addons and falling back to defaults for unknown/out-of-range values.files.h- New folder constantaddonPresetspointing to<RTTR_USERDATA>/PRESETS.const_gui_ids.h- NewCGI_ADDON_PRESETSGUI ID for the preset dialogs.Presets are stored as
.inifiles (usinglibsiedler2) in<RTTR_USERDATA>/PRESETS/. The save dialog validates the name (strips path components, rejects Windows reserved device names, prompts before overwrite). Load validates the file format and falls back to addon defaults for any missing entries.This PR probably also allows to solve issue #587, which reports that addon configurations configured while hosting a game are not preserved - players can now explicitly save a preset before a session and reload it next time.
Edit: Resolves #587