refactor storage to use QIODevice
Summary by Sourcery
Refactor map loading and saving to work with generic QIODevice-based sources and destinations instead of direct QFile/file paths.
New Features:
- Introduce MapSource to represent map input from files or in-memory buffers for loading and merging maps.
- Introduce MapDestination to represent map save targets, supporting both files and web-export directories.
Enhancements:
- Update MainWindow async load/merge/save flows and storage backends to operate on QIODevice abstractions rather than raw file paths or QFile pointers.
- Improve error reporting and status messages when opening or saving maps fails, including directory writability checks for web exports.
- Simplify and standardize suggested filenames in save/export dialogs across different map formats.
Build:
- Register new MapSource and MapDestination sources in the CMake build configuration.
Reviewer's Guide
Refactors map loading and saving to operate on generic QIODevice-based MapSource/MapDestination abstractions instead of working directly with QFile/FileSaver and raw filenames, enabling in‑memory buffers and directory destinations while centralizing detection, error handling, and device management.
Sequence diagram for loading a map via MapSource and QIODevice
sequenceDiagram
actor User
participant MainWindow
participant QFileDialog
participant MapSource
participant ProgressCounter
participant AbstractMapStorage
participant AsyncLoader
participant AsyncTask
participant FileFormatHelper
participant QIODevice
User ->> MainWindow: slot_open()
MainWindow ->> QFileDialog: getOpenFileName()
QFileDialog -->> MainWindow: fileName
alt fileName empty
MainWindow ->> MainWindow: showStatusShort(No filename provided)
MainWindow -->> User: return
else fileName provided
MainWindow ->> MapSource: alloc(fileName, none)
MapSource -->> MainWindow: shared_ptr_MapSource
MainWindow ->> MainWindow: loadFile(source)
MainWindow ->> MainWindow: tryStartNewAsync()
MainWindow ->> MainWindow: forceNewFile()
MainWindow ->> ProgressCounter: ctor()
ProgressCounter -->> MainWindow: shared_ptr_ProgressCounter
MainWindow ->> MainWindow: getLoadOrMergeMapStorage(pc, source)
MainWindow ->> AbstractMapStorage: Data(pc, source)
AbstractMapStorage -->> MainWindow: storage_data
MainWindow ->> MapSource: getIODevice()
MapSource -->> MainWindow: shared_ptr_QIODevice
loop iterate_formats
MainWindow ->> QIODevice: seek(0)
MainWindow ->> FileFormatHelper: detect(*device)
FileFormatHelper ->> QIODevice: read header
FileFormatHelper -->> MainWindow: format_match_or_not
end
MainWindow -->> MainWindow: create AbstractMapStorage for detected format
MainWindow ->> AsyncLoader: ctor(pc, this, fileName, device, storage)
AsyncLoader -->> MainWindow: asyncLoader_instance
MainWindow ->> AsyncTask: begin(asyncLoader_instance)
AsyncTask -->> MainWindow: started
end
Sequence diagram for saving a map via MapDestination and QIODevice
sequenceDiagram
actor User
participant MainWindow
participant QFileDialog
participant MapDestination
participant ProgressCounter
participant AbstractMapStorage
participant AsyncSaver
participant AsyncTask
participant QIODevice
User ->> MainWindow: slot_saveAs()
MainWindow ->> QFileDialog: getSaveFileName()
QFileDialog -->> MainWindow: fileName
alt fileName empty
MainWindow ->> MainWindow: showStatusShort(No filename provided)
MainWindow -->> User: return false
else fileName provided
MainWindow ->> MapDestination: alloc(fileName, SaveFormatEnum_MM2)
MapDestination -->> MainWindow: pDest
MainWindow ->> MainWindow: saveFile(pDest, SaveModeEnum_FULL, SaveFormatEnum_MM2)
MainWindow ->> ProgressCounter: ctor()
ProgressCounter -->> MainWindow: shared_ptr_ProgressCounter
MainWindow ->> MainWindow: create AbstractMapStorage::Data(pc, pDest)
MainWindow ->> AbstractMapStorage: construct storage for format
AbstractMapStorage -->> MainWindow: storage
MainWindow ->> AsyncSaver: ctor(pc, this, pDest, storage, mode, format)
AsyncSaver -->> MainWindow: asyncSaver_instance
MainWindow ->> AsyncTask: begin(asyncSaver_instance)
AsyncTask -->> MainWindow: started
Note over AsyncSaver,AbstractMapStorage: async background_save
AsyncSaver ->> AbstractMapStorage: saveData(mapData, baseMapOnly)
AbstractMapStorage ->> QIODevice: getDevice()
QIODevice -->> AbstractMapStorage: writable_device
AbstractMapStorage ->> QIODevice: write map bytes
AbstractMapStorage -->> AsyncSaver: success_flag
AsyncSaver ->> MapDestination: finalize(success)
MapDestination ->> QIODevice: close via FileSaver or directory
MapDestination -->> AsyncSaver: done
AsyncSaver ->> MainWindow: show success or error dialog
end
Class diagram for new MapSource and MapDestination based storage abstractions
classDiagram
class MapSource {
-QString m_fileName
-shared_ptr~QIODevice~ m_device
+static shared_ptr~MapSource~ alloc(QString fileName, optional~QByteArray~ fileContent)
+MapSource(Badge_MapSource badge, QString fileName, shared_ptr~QIODevice~ device)
+shared_ptr~QIODevice~ getIODevice()
+const QString & getFileName() const
}
class SaveModeEnum {
<<enumeration>>
FULL
BASEMAP
}
class SaveFormatEnum {
<<enumeration>>
MM2
MM2XML
WEB
MMP
}
class MapDestination {
-QString m_fileName
-shared_ptr~FileSaver~ m_fileSaver
+static shared_ptr~MapDestination~ alloc(QString fileName, SaveFormatEnum format)
+MapDestination(Badge_MapDestination badge, QString fileName, shared_ptr~FileSaver~ fileSaver)
+bool isFileNative() const
+bool isDirectory() const
+const QString & getFileName() const
+shared_ptr~QIODevice~ getIODevice() const
+void finalize(bool success)
}
class AbstractMapStorage_Data {
+shared_ptr~ProgressCounter~ progressCounter
+shared_ptr~MapSource~ loadSource
+shared_ptr~MapDestination~ saveDestination
+Data(shared_ptr~ProgressCounter~ pc, shared_ptr~MapSource~ src)
+Data(shared_ptr~ProgressCounter~ pc, shared_ptr~MapDestination~ dest)
}
class AbstractMapStorage {
-AbstractMapStorage_Data m_data
+AbstractMapStorage(AbstractMapStorage_Data data, QObject *parent)
+bool canLoad() const
+bool canSave() const
+optional~RawMapLoadData~ loadData()
+bool saveData(const MapData & mapData, bool baseMapOnly)
+const QString & getFilename() const
+ProgressCounter & getProgressCounter() const
+QIODevice & getDevice() const
<<abstract>> bool virt_canLoad() const
<<abstract>> bool virt_canSave() const
<<abstract>> optional~RawMapLoadData~ virt_loadData()
<<abstract>> bool virt_saveData(const MapLoadData & mapData)
}
class FileSaver {
+void open(QString fileName)
+void close()
+shared_ptr~QFile~ getSharedFile() const
}
class QIODevice {
}
class ProgressCounter {
}
MapSource ..> QIODevice : uses
MapDestination ..> FileSaver : wraps
MapDestination ..> QIODevice : exposes
AbstractMapStorage_Data --> MapSource : loadSource
AbstractMapStorage_Data --> MapDestination : saveDestination
AbstractMapStorage o-- AbstractMapStorage_Data : has
AbstractMapStorage ..> MapSource : load via
AbstractMapStorage ..> MapDestination : save via
MapDestination ..> SaveFormatEnum
AbstractMapStorage ..> SaveModeEnum
AbstractMapStorage ..> ProgressCounter
Class diagram for MainWindow async IO refactor using QIODevice, MapSource, and MapDestination
classDiagram
class MainWindow {
+bool saveFile(shared_ptr~MapDestination~ pDest, SaveModeEnum mode, SaveFormatEnum format)
+void loadFile(shared_ptr~MapSource~ source)
+unique_ptr~AbstractMapStorage~ getLoadOrMergeMapStorage(shared_ptr~ProgressCounter~ pc, shared_ptr~MapSource~ source)
+void slot_open()
+void slot_merge()
+bool slot_save()
+bool slot_saveAs()
+bool slot_exportBaseMap()
+bool slot_exportMm2xmlMap()
+bool slot_exportWebMap()
+bool slot_exportMmpMap()
+void slot_reload()
}
class AsyncBase {
+AsyncBase(shared_ptr~ProgressCounter~ pc)
+void reset()
}
class AsyncTask {
+void begin(unique_ptr~AsyncBase~ task)
+void reset()
}
class AsyncHelper {
<<struct>>
+typedef shared_ptr~QIODevice~ SharedDevice
+typedef unique_ptr~AbstractMapStorage~ UniqueStorage
+MainWindow & mainWindow
+QString fileName
+SharedDevice pDevice
+AsyncHelper(shared_ptr~ProgressCounter~ pc, MainWindow & mw, QString name, SharedDevice pd, UniqueStorage ps, QString dialogText, CancelDispositionEnum allow_cancel)
+void reset()
}
class AsyncLoader {
+AsyncLoader(shared_ptr~ProgressCounter~ pc, MainWindow & mw, QString name, AsyncHelper_SharedDevice pd, AsyncHelper_UniqueStorage ps)
+Result work() noexcept
}
class AsyncMerge {
+AsyncMerge(shared_ptr~ProgressCounter~ pc, MainWindow & mw, QString name, AsyncHelper_SharedDevice pd, AsyncHelper_UniqueStorage ps)
+Result work() noexcept
}
class AsyncSaver {
+SaveModeEnum mode
+SaveFormatEnum format
+shared_ptr~MapDestination~ pMapDestination
+AsyncSaver(shared_ptr~ProgressCounter~ pc, MainWindow & mw, shared_ptr~MapDestination~ pDest, unique_ptr~AbstractMapStorage~ ps, SaveModeEnum mode, SaveFormatEnum format)
+Result work() noexcept
+void finish_saving(bool success)
}
class MapSource {
}
class MapDestination {
}
class QIODevice {
}
class ProgressCounter {
}
class AbstractMapStorage {
}
class CancelDispositionEnum {
<<enumeration>>
Forbid
Allow
}
MainWindow --> AsyncTask : manages
AsyncHelper --|> AsyncBase
AsyncLoader --|> AsyncHelper
AsyncMerge --|> AsyncHelper
AsyncSaver --|> AsyncHelper
AsyncHelper --> MainWindow : holds_reference
AsyncHelper --> QIODevice : pDevice
AsyncHelper --> AbstractMapStorage : pStorage
MainWindow ..> MapSource : loadFile
MainWindow ..> MapDestination : saveFile
MainWindow ..> AsyncLoader
MainWindow ..> AsyncMerge
MainWindow ..> AsyncSaver
MainWindow ..> ProgressCounter
AsyncSaver --> MapDestination : pMapDestination
AsyncSaver ..> SaveModeEnum
AsyncSaver ..> SaveFormatEnum
AbstractMapStorage ..> QIODevice : getDevice
AsyncBase ..> ProgressCounter
File-Level Changes
| Change | Details | Files |
|---|---|---|
| Introduce MapSource/MapDestination abstractions and wire MainWindow async load/save flows to use them instead of QFile/FileSaver and raw filenames. |
|
src/mapstorage/MapSource.hsrc/mapstorage/MapSource.cppsrc/mapstorage/MapDestination.hsrc/mapstorage/MapDestination.cppsrc/mainwindow/mainwindow-async.cppsrc/mainwindow/mainwindow-saveslots.cppsrc/mainwindow/mainwindow.cpp |
| Refactor AbstractMapStorage and concrete storages to operate on QIODevice via MapSource/MapDestination, decoupling them from QFile specifics. |
|
src/mapstorage/abstractmapstorage.hsrc/mapstorage/abstractmapstorage.cppsrc/mapstorage/mapstorage.cppsrc/mapstorage/mapstorage.hsrc/mapstorage/XmlMapStorage.cppsrc/mapstorage/PandoraMapStorage.cppsrc/mapstorage/MmpMapStorage.cpp |
| Make file format detection and MM2 version probing work on QIODevice rather than filenames, ensuring reusable streams and correct rewinding. |
|
src/mapstorage/mapstorage.hsrc/mapstorage/mapstorage.cppsrc/mainwindow/mainwindow-async.cpp |
| Improve user-facing error handling and messaging for load and merge operations while removing now-redundant helpers. |
|
src/mainwindow/mainwindow.cppsrc/mainwindow/mainwindow-async.cpp |
| Adjust save dialog behavior and suggested filenames to use baseName/suffix logic instead of regex replacements and to align with new MapDestination usage. |
|
src/mainwindow/mainwindow-saveslots.cpp |
| Miscellaneous cleanup related to the refactor. |
|
src/mainwindow/mainwindow-async.cppsrc/mainwindow/mainwindow.hsrc/CMakeLists.txt |
Tips and commands
Interacting with Sourcery
-
Trigger a new review: Comment
@sourcery-ai reviewon the pull request. - Continue discussions: Reply directly to Sourcery's review comments.
-
Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with
@sourcery-ai issueto create an issue from it. -
Generate a pull request title: Write
@sourcery-aianywhere in the pull request title to generate a title at any time. You can also comment@sourcery-ai titleon the pull request to (re-)generate the title at any time. -
Generate a pull request summary: Write
@sourcery-ai summaryanywhere in the pull request body to generate a PR summary at any time exactly where you want it. You can also comment@sourcery-ai summaryon the pull request to (re-)generate the summary at any time. -
Generate reviewer's guide: Comment
@sourcery-ai guideon the pull request to (re-)generate the reviewer's guide at any time. -
Resolve all Sourcery comments: Comment
@sourcery-ai resolveon the pull request to resolve all Sourcery comments. Useful if you've already addressed all the comments and don't want to see them anymore. -
Dismiss all Sourcery reviews: Comment
@sourcery-ai dismisson the pull request to dismiss all existing Sourcery reviews. Especially useful if you want to start fresh with a new review - don't forget to comment@sourcery-ai reviewto trigger a new review!
Customizing Your Experience
Access your dashboard to:
- Enable or disable review features such as the Sourcery-generated pull request summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.
Getting Help
- Contact our support team for questions or feedback.
- Visit our documentation for detailed guides and information.
- Keep in touch with the Sourcery team by following us on X/Twitter, LinkedIn or GitHub.
Codecov Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 66.37%. Comparing base (26d1a9d) to head (8a652df).
:warning: Report is 124 commits behind head on master.
Additional details and impacted files
@@ Coverage Diff @@
## master #411 +/- ##
==========================================
- Coverage 66.48% 66.37% -0.12%
==========================================
Files 85 86 +1
Lines 4186 3937 -249
Branches 255 257 +2
==========================================
- Hits 2783 2613 -170
+ Misses 1403 1324 -79
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.