25#include <QGuiApplication>
27#include <QElapsedTimer>
33#include <QApplication>
36#include <QQmlAbstractUrlInterceptor>
37#include <QFileSystemWatcher>
38#include <QDirIterator>
47class QmlDiskInterceptor :
public QQmlAbstractUrlInterceptor {
49 explicit QmlDiskInterceptor(
const QString& root) : m_root(root) {}
50 QUrl intercept(
const QUrl& url, DataType)
override {
51 if(url.scheme() != QLatin1String(
"qrc"))
return url;
52 const QString path = url.path();
53 if(!path.endsWith(QLatin1String(
".qml")) && !path.endsWith(QLatin1String(
".js")))
55 const QString disk = m_root + path;
56 if(QFileInfo::exists(disk))
return QUrl::fromLocalFile(disk);
95 qDebug() <<
"[MainWindow] setupUi —" << t.elapsed() <<
"ms";
98 MainWindow::instance =
this;
103 qDebug() <<
"[MainWindow] FileManagement —" << t.elapsed() <<
"ms";
107 qDebug() <<
"[MainWindow] injectIntoQML —" << t.elapsed() <<
"ms";
111 qDebug() <<
"[MainWindow] setupProviders —" << t.elapsed() <<
"ms";
116 connect(ui.app, &QQuickWidget::statusChanged,
this, [
this](QQuickWidget::Status status) {
117 if (status == QQuickWidget::Error) {
119 for (const auto& err : ui.app->errors())
120 msg += err.toString() +
"\n";
121 qCritical() <<
"[QML]" << msg;
123 QMessageBox::critical(this,
"QML Error", msg);
131 if(QApplication::arguments().contains(QStringLiteral(
"--hot")))
136 ui.app->setSource(QUrl(QStringLiteral(
"qrc:/ui/app/App.qml")));
137 qDebug() <<
"[MainWindow] setSource —" << t.elapsed() <<
"ms";
146 reUpdateRecentFiles(file->getRecentFiles());
147 onPathChanged(file->getPath());
149 qDebug() <<
"[MainWindow] constructor done —" << t.elapsed() <<
"ms";
169 return MainWindow::instance;
178 const QImage img = ui.app->grabFramebuffer();
181 return img.save(path);
186 QObject* root = ui.app->rootObject();
189 QObject* appWindow = root->findChild<QObject*>(QStringLiteral(
"appWindow"));
190 if(appWindow ==
nullptr)
193 const bool ok = QMetaObject::invokeMethod(
194 appWindow,
"debugOpenPartyDetails",
195 Q_RETURN_ARG(QVariant, ret), Q_ARG(QVariant, index));
196 return ok && ret.toBool();
201 return ui.app->rootObject();
204void MainWindow::setupHotReload()
207#ifdef PSE_QML_SOURCE_DIR
208 const QString root = QStringLiteral(PSE_QML_SOURCE_DIR);
212 if(root.isEmpty() || !QDir(root + QStringLiteral(
"/ui")).exists()) {
213 qWarning() <<
"[hot-reload] QML source dir not found:" << root <<
"-- staying on qrc.";
216 ui.app->engine()->addUrlInterceptor(
new QmlDiskInterceptor(root));
218 m_qmlWatcher =
new QFileSystemWatcher(
this);
220 QDirIterator it(root + QStringLiteral(
"/ui"), QStringList{ QStringLiteral(
"*.qml"), QStringLiteral(
"*.js") },
221 QDir::Files, QDirIterator::Subdirectories);
222 while(it.hasNext()) files << it.next();
223 if(!files.isEmpty()) m_qmlWatcher->addPaths(files);
224 connect(m_qmlWatcher, &QFileSystemWatcher::fileChanged,
this, [
this](
const QString& p) {
226 if(!m_qmlWatcher->files().contains(p) && QFileInfo::exists(p))
227 m_qmlWatcher->addPath(p);
228 if(m_reloadPending)
return;
229 m_reloadPending =
true;
230 QTimer::singleShot(160,
this, [
this]{ m_reloadPending =
false;
reloadQml(); });
232 qInfo() <<
"[hot-reload] watching" << files.size() <<
"QML files under" << (root + QStringLiteral(
"/ui"));
255 if(it.value() == top) { currentName = it.key();
break; }
260 const QUrl src = ui.app->source();
261 ui.app->setSource(QUrl());
262 ui.app->setSource(src.isValid() ? src : QUrl(QStringLiteral(
"qrc:/ui/app/App.qml")));
263 qInfo() <<
"[hot-reload] reloaded" << (src.isValid() ? src.toString() : QStringLiteral(
"App.qml"))
264 <<
"-- restoring screen:" << (currentName.isEmpty() ? QStringLiteral(
"(none)") : currentName);
269 const bool restore = !currentName.isEmpty()
270 && currentName != QStringLiteral(
"home")
281 QTimer* timer =
new QTimer(
this);
282 int* tries =
new int(0);
283 const QString target = currentName;
284 connect(timer, &QTimer::timeout,
this, [
this, timer, tries, target]() {
286 const bool ready = (brg !=
nullptr) &&
Router::stack.size() >= 2;
287 if(!ready && ++(*tries) < 200)
290 timer->deleteLater();
295 if(target == QStringLiteral(
"pokemonDetails"))
299 qInfo() <<
"[hot-reload] restored screen" << target;
301 timer->setInterval(50);
306void MainWindow::reUpdateRecentFiles(QList<QString> files)
315 QString
file{files.at(i)};
323void MainWindow::onRecentFileClick()
325 QShortcut* shortcut{qobject_cast<QShortcut*>(sender())};
326 var8 index{
static_cast<var8>(shortcut->property(
"index").toInt())};
327 file->openFileRecent(index);
330void MainWindow::onPathChanged(QString path)
333 this->setWindowTitle(
"Pokered Save Editor - New File");
335 this->setWindowTitle(
"Pokered Save Editor - " + path);
338void MainWindow::closeEvent(QCloseEvent *event)
344void MainWindow::saveState()
346 settings.beginGroup(
"WindowState");
347 settings.setValue(
"size", this->size());
348 settings.setValue(
"pos", this->pos());
352void MainWindow::loadState()
354 settings.beginGroup(
"WindowState");
355 QSize savedSize = settings.value(
"size", QSize(1130, 740)).toSize();
356 QPoint savedPos = settings.value(
"pos", QPoint(200, 200)).toPoint();
359 this->resize(savedSize);
363 bool onScreen =
false;
364 const QPoint titleBarPt = savedPos + QPoint(savedSize.width() / 2, 10);
365 for (
const QScreen* screen : QGuiApplication::screens()) {
366 if (screen->availableGeometry().contains(titleBarPt)) {
371 this->move(onScreen ? savedPos : QPoint(200, 200));
374void MainWindow::setupShortcuts()
385 connect(
recentFileShortcuts[i], &QShortcut::activated,
this, &MainWindow::onRecentFileClick);
395 for (
auto it = keymap.constBegin(); it != keymap.constEnd(); ++it)
396 os.insert(it.key(),
new QShortcut(it.value(),
this));
401 for (
auto it = actions.constBegin(); it != actions.constEnd(); ++it) {
402 QShortcut* sc = os.value(it.key(),
nullptr);
404 const std::function<void()> verb = it.value();
405 connect(sc, &QShortcut::activated,
this, [verb]{ verb(); });
409void MainWindow::setupProviders()
411 auto engine = ui.app->engine();
412 engine->addImageProvider(
"tileset",
new TilesetProvider);
413 engine->addImageProvider(
"font",
new FontPreviewProvider(
file->data->dataExpanded));
416void MainWindow::injectIntoQML()
418 auto* context = ui.app->rootContext();
420 context->setContextProperty(
"brg",
bridge);
430void MainWindow::ssConnect()
The single QML<->C++ doorway – everything the UI touches hangs off here.
void qmlProtect(const QQmlEngine *const engine) const
Pin the DB aggregate (and every sub-DB) to C++ ownership so QML never GCs them.
static DB * inst()
< Raw parsed JSON assets behind every DB.
Owns the on-disk side of a save: the current path, the recent-files list, and the live SaveFile.
protected::void pathChanged(QString newPath, QString oldPath)
The active path changed.
void recentFilesChanged(QList< QString > files)
The recent-files list changed.
The top-level window – a QMainWindow hosting the QML UI in a QQuickWidget.
bool saveShot(const QString &path)
DEBUG: render the live QML view to an image file (focus/occlusion-independent).
bool debugOpenPartyDetails(int index)
DEBUG: open the details editor for party mon index (drives the QML AppWindow.debugOpenPartyDetails).
static QQmlEngine * engine
The QML engine behind the hosted QQuickWidget.
QShortcut * recentFileShortcuts[5]
Ctrl+1..5 open-recent shortcuts.
QHash< QString, QShortcut * > otherShortcuts
Other global keyboard shortcuts by name.
QObject * qmlRootObject()
DEBUG: the root QML object of the hosted view (for the debug control server's object lookups).
MainWindow(QWidget *parent=nullptr)
< The live save controller.
static MainWindow * getInstance()
The single MainWindow instance.
static Bridge * bridge
The brg aggregate (created here, injected into QML).
void reloadQml()
DEBUG (–hot): clear the QML cache and reload the view from the source files on disk (live QML refresh...
void changeScreen(QString name)
Navigate to the registered screen name.
static QVector< Screen * > stack
The live navigation stack.
void closeScreen()
Close the top screen.
static QHash< QString, Screen * > screens
The registry of named screens.
Project-wide fixed-width integer aliases (var8, var16, ...).
var8e var8
Everyday 8-bit alias. Exact (not "fastest") to dodge the pointer-width bug noted above.
constexpr var8 MAX_RECENT_FILES
How many recent paths to remember.
QHash< QString, QKeySequence > shortcutKeyMap()
Named global shortcuts: action id -> key sequence.
QHash< QString, std::function< void()> > shortcutActions(FileManagement *file, std::function< void()> onExit)
What each named shortcut DOES, action id -> callable, over a live FileManagement.
QKeySequence recentFileShortcutKey(int i)
Recent-file shortcut i (0..MAX_RECENT_FILES-1) == Ctrl+Shift+(0+i), i.e.
Single source of truth for the global keyboard shortcut KEY SEQUENCES.
One registered screen: its QML url, title, and modal/home-button flags.