From 72ff342d6325cab42cc3d8401b4fac5b2f7eff3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 12 Apr 2018 01:44:51 +0200 Subject: [PATCH] GH-2053 basics of the servers.dat management --- application/CMakeLists.txt | 3 + application/InstancePageProvider.h | 2 + application/InstanceWindow.cpp | 2 +- application/pages/instance/ServersPage.cpp | 743 ++++++++++++++++++ application/pages/instance/ServersPage.h | 87 ++ application/pages/instance/ServersPage.ui | 207 +++++ .../multimc/128x128/unknown_server.png | Bin 0 -> 11085 bytes application/resources/multimc/index.theme | 5 + application/resources/multimc/multimc.qrc | 3 + application/widgets/PageContainer.cpp | 15 +- application/widgets/PageContainer.h | 1 + 11 files changed, 1064 insertions(+), 4 deletions(-) create mode 100644 application/pages/instance/ServersPage.cpp create mode 100644 application/pages/instance/ServersPage.h create mode 100644 application/pages/instance/ServersPage.ui create mode 100644 application/resources/multimc/128x128/unknown_server.png diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index d67540b9..c0268da5 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -100,6 +100,8 @@ SET(MULTIMC_SOURCES pages/instance/ScreenshotsPage.h pages/instance/OtherLogsPage.cpp pages/instance/OtherLogsPage.h + pages/instance/ServersPage.cpp + pages/instance/ServersPage.h pages/instance/LegacyUpgradePage.cpp pages/instance/LegacyUpgradePage.h pages/instance/WorldListPage.cpp @@ -231,6 +233,7 @@ SET(MULTIMC_UIS pages/instance/ScreenshotsPage.ui pages/instance/OtherLogsPage.ui pages/instance/LegacyUpgradePage.ui + pages/instance/ServersPage.ui pages/instance/WorldListPage.ui # Global settings pages diff --git a/application/InstancePageProvider.h b/application/InstancePageProvider.h index 9dda7859..b7a9513c 100644 --- a/application/InstancePageProvider.h +++ b/application/InstancePageProvider.h @@ -15,6 +15,7 @@ #include "pages/instance/OtherLogsPage.h" #include "pages/instance/LegacyUpgradePage.h" #include "pages/instance/WorldListPage.h" +#include "pages/instance/ServersPage.h" class InstancePageProvider : public QObject, public BasePageProvider @@ -43,6 +44,7 @@ public: values.append(new TexturePackPage(onesix.get())); values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix.get(), onesix->worldList(), "worlds", "worlds", tr("Worlds"), "Worlds")); + values.append(new ServersPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->minecraftRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix.get())); } diff --git a/application/InstanceWindow.cpp b/application/InstanceWindow.cpp index 5895ca3a..b36781a7 100644 --- a/application/InstanceWindow.cpp +++ b/application/InstanceWindow.cpp @@ -181,7 +181,7 @@ void InstanceWindow::closeEvent(QCloseEvent *event) bool InstanceWindow::saveAll() { - return m_container->prepareToClose(); + return m_container->saveAll(); } void InstanceWindow::on_btnKillMinecraft_clicked() diff --git a/application/pages/instance/ServersPage.cpp b/application/pages/instance/ServersPage.cpp new file mode 100644 index 00000000..f7ddc7da --- /dev/null +++ b/application/pages/instance/ServersPage.cpp @@ -0,0 +1,743 @@ +#include "ServersPage.h" +#include "ui_ServersPage.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. + +struct Server +{ + // Types + enum class AcceptsTextures : int + { + ASK = 0, + ALWAYS = 1, + NEVER = 2 + }; + + // Methods + Server() + { + m_name = QObject::tr("Minecraft Server"); + } + Server(const QString & name, const QString & address) + { + m_name = name; + m_address = address; + } + Server(nbt::tag_compound& server) + { + std::string addressStr(server["ip"]); + m_address = QString::fromUtf8(addressStr.c_str()); + + std::string nameStr(server["name"]); + m_name = QString::fromUtf8(nameStr.c_str()); + + if(server["icon"]) + { + std::string base64str(server["icon"]); + m_icon = QByteArray::fromBase64(base64str.c_str()); + } + + if(server.has_key("acceptTextures", nbt::tag_type::Byte)) + { + bool value = server["acceptTextures"].as().get(); + if(value) + { + m_acceptsTextures = AcceptsTextures::ALWAYS; + } + else + { + m_acceptsTextures = AcceptsTextures::NEVER; + } + } + } + + void serialize(nbt::tag_compound& server) + { + server.insert("name", m_name.toUtf8().toStdString()); + server.insert("ip", m_address.toUtf8().toStdString()); + if(m_icon.size()) + { + server.insert("icon", m_icon.toBase64().toStdString()); + } + if(m_acceptsTextures != AcceptsTextures::ASK) + { + server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS)); + } + } + + // Data - persistent and user changeable + QString m_name; + QString m_address; + AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK; + + // Data - persistent and automatically updated + QByteArray m_icon; + + // Data - temporary + bool m_checked = false; + bool m_up = false; + QString m_motd; // https://mctools.org/motd-creator + int m_ping = 0; + int m_currentPlayers = 0; + int m_maxPlayers = 0; +}; + +static std::unique_ptr parseServersDat(const QString& filename) +{ + try + { + QByteArray input = FS::read(filename); + std::istringstream foo(std::string(input.constData(), input.size())); + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch(...) + { + return nullptr; + } +} + +static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo) +{ + try + { + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int) s.str().size() ); + FS::write(filename, val); + return true; + } + catch(...) + { + return false; + } +} + +class ServersModel: public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + ServerPtrRole = Qt::UserRole, + }; + explicit ServersModel(const QString &path, QObject *parent = 0) + : QAbstractListModel(parent) + { + m_path = path; + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged); + m_saveTimer.setSingleShot(true); + m_saveTimer.setInterval(5000); + connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); + } + virtual ~ServersModel() {}; + + void observe() + { + if(m_observed) + { + return; + } + m_observed = true; + + if(!m_loaded) + { + load(); + } + + updateFSObserver(); + } + + void unobserve() + { + if(!m_observed) + { + return; + } + m_observed = false; + + updateFSObserver(); + } + + void lock() + { + if(m_locked) + { + return; + } + saveNow(); + + m_locked = true; + updateFSObserver(); + } + + void unlock() + { + if(!m_locked) + { + return; + } + m_locked = false; + + updateFSObserver(); + } + + int addEmptyRow(int position) + { + if(m_locked) + { + return -1; + } + if(position < 0 || position >= rowCount()) + { + position = rowCount(); + } + beginInsertRows(QModelIndex(), position, position); + m_servers.insert(position, Server()); + endInsertRows(); + scheduleSave(); + return position; + } + + bool removeRow(int row) + { + if(m_locked) + { + return false; + } + if(row < 0 || row >= rowCount()) + { + return false; + } + beginRemoveRows(QModelIndex(), row, row); + m_servers.removeAt(row); + endRemoveRows(); // does absolutely nothing, the selected server stays as the next line... + scheduleSave(); + return true; + } + + bool moveUp(int row) + { + if(m_locked) + { + return false; + } + if(row <= 0) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); + m_servers.swap(row-1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + bool moveDown(int row) + { + if(m_locked) + { + return false; + } + int count = rowCount(); + if(row + 1 >= count) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); + m_servers.swap(row+1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if (section < 0 || section >= COLUMN_COUNT) + return QVariant(); + + if(role == Qt::DisplayRole) + { + switch(section) + { + case 0: + return tr("Name"); + case 1: + return tr("Address"); + case 2: + return tr("Latency"); + } + } + + return QAbstractListModel::headerData(section, orientation, role); + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + if(column < 0 || column >= COLUMN_COUNT) + return QVariant(); + + if (row < 0 || row >= m_servers.size()) + return QVariant(); + + switch(column) + { + case 0: + switch (role) + { + case Qt::DecorationRole: + { + auto & bytes = m_servers[row].m_icon; + if(bytes.size()) + { + QPixmap px; + if(px.loadFromData(bytes)) + return QIcon(px); + } + return MMC->getThemedIcon("unknown_server"); + } + case Qt::DisplayRole: + return m_servers[row].m_name; + case ServerPtrRole: + return QVariant::fromValue((void *)&m_servers[row]); + default: + return QVariant(); + } + case 1: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_address; + default: + return QVariant(); + } + case 2: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_ping; + default: + return QVariant(); + } + default: + return QVariant(); + } + } + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + return m_servers.size(); + } + int columnCount(const QModelIndex & parent) const override + { + return COLUMN_COUNT; + } + + Server * at(int index) + { + if(index < 0 || index >= rowCount()) + { + return nullptr; + } + return &m_servers[index]; + } + + void setName(int row, const QString & name) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_name == name) + { + return; + } + server->m_name = name; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAddress(int row, const QString & address) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_address == address) + { + return; + } + server->m_address = address; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAcceptsTextures(int row, Server::AcceptsTextures textures) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_acceptsTextures == textures) + { + return; + } + server->m_acceptsTextures = textures; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void load() + { + cancelSave(); + beginResetModel(); + QList servers; + auto serversDat = parseServersDat(serversPath()); + if(serversDat) + { + auto &serversList = serversDat->at("servers").as(); + for(auto iter = serversList.begin(); iter != serversList.end(); iter++) + { + auto & serverTag = (*iter).as(); + Server s(serverTag); + servers.append(s); + } + } + m_servers.swap(servers); + m_loaded = true; + endResetModel(); + } + + void saveNow() + { + if(saveIsScheduled()) + { + save_internal(); + } + } + + +public slots: + void dirChanged(const QString& path) + { + qDebug() << "Changed:" << path; + load(); + } + void fileChanged(const QString& path) + { + qDebug() << "Changed:" << path; + } + +private slots: + void save_internal() + { + cancelSave(); + qDebug() << "Server list save is performed for" << m_path; + + nbt::tag_compound out; + nbt::tag_list list; + for(auto & server: m_servers) + { + nbt::tag_compound serverNbt; + server.serialize(serverNbt); + list.push_back(std::move(serverNbt)); + } + out.insert("servers", nbt::value(std::move(list))); + + if(!serializeServerDat(serversPath(), &out)) + { + qDebug() << "Failed to save server list:" << m_path << "Will try again."; + scheduleSave(); + } + } + +private: + void scheduleSave() + { + if(!m_loaded) + { + qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path; + return; + } + if(!m_dirty) + { + m_dirty = true; + qDebug() << "Server list save is scheduled for" << m_path; + } + m_saveTimer.start(); + } + + void cancelSave() + { + m_dirty = false; + m_saveTimer.stop(); + } + + bool saveIsScheduled() const + { + return m_dirty; + } + + void updateFSObserver() + { + bool observingFS = m_watcher->directories().contains(m_path); + if(m_observed && m_locked) + { + if(!observingFS) + { + qWarning() << "Will watch" << m_path; + if(!m_watcher->addPath(m_path)) + { + qWarning() << "Failed to start watching" << m_path; + } + } + } + else + { + if(observingFS) + { + qWarning() << "Will stop watching" << m_path; + if(!m_watcher->removePath(m_path)) + { + qWarning() << "Failed to stop watching" << m_path; + } + } + } + } + + QString serversPath() + { + QFileInfo foo(FS::PathCombine(m_path, "servers.dat")); + return foo.canonicalFilePath(); + } + +private: + bool m_loaded = false; + bool m_locked = false; + bool m_observed = false; + bool m_dirty = false; + QString m_path; + QList m_servers; + QFileSystemWatcher *m_watcher = nullptr; + QTimer m_saveTimer; +}; + +ServersPage::ServersPage(MinecraftInstance * inst, QWidget* parent) + : QWidget(parent), ui(new Ui::ServersPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + m_inst = inst; + m_model = new ServersModel(inst->minecraftRoot(), this); + ui->serversView->setIconSize(QSize(64,64)); + ui->serversView->setModel(m_model); + auto head = ui->serversView->header(); + if(head->count()) + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + { + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + } + + auto selectionModel = ui->serversView->selectionModel(); + connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); + connect(m_inst, &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); + connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); + connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); + connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); + + m_locked = m_inst->isRunning(); + if(m_locked) + { + m_model->lock(); + } + + updateState(); +} + +ServersPage::~ServersPage() +{ + m_model->saveNow(); +} + +void ServersPage::on_RunningState_changed(bool running) +{ + if(m_locked == running) + { + return; + } + m_locked = running; + if(m_locked) + { + m_model->lock(); + } + else + { + m_model->unlock(); + } + updateState(); +} + +void ServersPage::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + int nextServer = -1; + if (!current.isValid()) + { + nextServer = -1; + } + else + { + nextServer = current.row(); + } + currentServer = nextServer; + updateState(); +} + +// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal. +void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last) +{ + if(currentServer < first) + { + // current was before the removal + return; + } + else if(currentServer >= first && currentServer <= last) + { + // current got removed... + return; + } + else + { + // current was past the removal + int count = last - first + 1; + currentServer -= count; + } +} + +void ServersPage::nameEdited(const QString& name) +{ + m_model->setName(currentServer, name); +} + +void ServersPage::addressEdited(const QString& address) +{ + m_model->setAddress(currentServer, address); +} + +void ServersPage::resourceIndexChanged(int index) +{ + auto acceptsTextures = Server::AcceptsTextures(index); + m_model->setAcceptsTextures(currentServer, acceptsTextures); +} + +void ServersPage::updateState() +{ + auto server = m_model->at(currentServer); + + bool serverEditEnabled = server && !m_locked; + ui->addressLine->setEnabled(serverEditEnabled); + ui->nameLine->setEnabled(serverEditEnabled); + ui->resourceComboBox->setEnabled(serverEditEnabled); + ui->moveDownBtn->setEnabled(serverEditEnabled); + ui->moveUpBtn->setEnabled(serverEditEnabled); + ui->removeBtn->setEnabled(serverEditEnabled); + + if(server) + { + ui->addressLine->setText(server->m_address); + ui->nameLine->setText(server->m_name); + ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures)); + } + else + { + ui->addressLine->setText(QString()); + ui->nameLine->setText(QString()); + ui->resourceComboBox->setCurrentIndex(0); + } + + ui->addBtn->setDisabled(m_locked); +} + +void ServersPage::openedImpl() +{ + m_model->observe(); +} + +void ServersPage::closedImpl() +{ + m_model->unobserve(); +} + +void ServersPage::on_addBtn_clicked() +{ + int position = m_model->addEmptyRow(currentServer + 1); + if(position < 0) + { + return; + } + // select the new row + ui->serversView->selectionModel()->setCurrentIndex( + m_model->index(position), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows + ); + currentServer = position; +} + +void ServersPage::on_removeBtn_clicked() +{ + m_model->removeRow(currentServer); +} + +void ServersPage::on_moveUpBtn_clicked() +{ + if(m_model->moveUp(currentServer)) + { + currentServer --; + } +} + +void ServersPage::on_moveDownBtn_clicked() +{ + if(m_model->moveDown(currentServer)) + { + currentServer ++; + } +} + +void ServersPage::on_refreshBtn_clicked() +{ + m_model->load(); +} + +#include "ServersPage.moc" diff --git a/application/pages/instance/ServersPage.h b/application/pages/instance/ServersPage.h new file mode 100644 index 00000000..6c812bd9 --- /dev/null +++ b/application/pages/instance/ServersPage.h @@ -0,0 +1,87 @@ +/* Copyright 2013-2018 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "pages/BasePage.h" +#include + +namespace Ui +{ +class ServersPage; +} + +struct Server; +class ServersModel; +class MinecraftInstance; + +class ServersPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ServersPage(MinecraftInstance *inst, QWidget *parent = 0); + virtual ~ServersPage(); + + void openedImpl() override; + void closedImpl() override; + + virtual QString displayName() const override + { + return tr("Servers"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("unknown_server"); + } + virtual QString id() const override + { + return "servers"; + } + virtual QString helpPage() const override + { + return "Servers-management"; + } +private: + void updateState(); + void scheduleSave(); + bool saveIsScheduled() const; + +private slots: + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void rowsRemoved(const QModelIndex &parent, int first, int last); + + void on_addBtn_clicked(); + void on_removeBtn_clicked(); + void on_moveUpBtn_clicked(); + void on_moveDownBtn_clicked(); + void on_refreshBtn_clicked(); + void on_RunningState_changed(bool running); + + void nameEdited(const QString & name); + void addressEdited(const QString & address); + void resourceIndexChanged(int index); + +private: // data + int currentServer = -1; + bool m_locked = true; + Ui::ServersPage *ui = nullptr; + ServersModel * m_model = nullptr; + MinecraftInstance * m_inst = nullptr; +}; + diff --git a/application/pages/instance/ServersPage.ui b/application/pages/instance/ServersPage.ui new file mode 100644 index 00000000..6d1a9bc0 --- /dev/null +++ b/application/pages/instance/ServersPage.ui @@ -0,0 +1,207 @@ + + + ServersPage + + + + 0 + 0 + 706 + 575 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + 0 + 0 + + + + Tab 1 + + + + + + + + + Ask to download + + + + + Always download + + + + + Never download + + + + + + + + Reso&urces + + + resourceComboBox + + + + + + + + + + &Name + + + nameLine + + + + + + + + + + Address + + + addressLine + + + + + + + + 0 + 0 + + + + true + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 64 + 64 + + + + false + + + false + + + + + + + + + + + &Add + + + + + + + &Remove + + + + + + + Move Up + + + + + + + Move Down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + + + + + + + tabWidget + serversView + nameLine + addressLine + resourceComboBox + addBtn + removeBtn + moveUpBtn + moveDownBtn + refreshBtn + + + + diff --git a/application/resources/multimc/128x128/unknown_server.png b/application/resources/multimc/128x128/unknown_server.png new file mode 100644 index 0000000000000000000000000000000000000000..ec98382d47272f9cebceb0d2aa8e4f187fc0b54f GIT binary patch literal 11085 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

qgOD9{EgU9=(_6 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 RfAg_O>dFA61_kSw{{tAM#r*&P literal 0 HcmV?d00001 diff --git a/application/resources/multimc/index.theme b/application/resources/multimc/index.theme index 0fe7e7d7..290f42fb 100644 --- a/application/resources/multimc/index.theme +++ b/application/resources/multimc/index.theme @@ -33,6 +33,11 @@ Size=50 [64x64] Size=64 +[128x128] +Size=128 +MinSize=33 +MaxSize=128 + [128x128/instances] Size=128 MinSize=33 diff --git a/application/resources/multimc/multimc.qrc b/application/resources/multimc/multimc.qrc index bea3a325..55cc601e 100644 --- a/application/resources/multimc/multimc.qrc +++ b/application/resources/multimc/multimc.qrc @@ -239,6 +239,9 @@ 48x48/log.png 64x64/log.png + + 128x128/unknown_server.png + scalable/screenshot-placeholder.svg diff --git a/application/widgets/PageContainer.cpp b/application/widgets/PageContainer.cpp index 98de57e8..c8c2b57a 100644 --- a/application/widgets/PageContainer.cpp +++ b/application/widgets/PageContainer.cpp @@ -218,10 +218,9 @@ void PageContainer::currentChanged(const QModelIndex ¤t) bool PageContainer::prepareToClose() { - for (auto page : m_model->pages()) + if(!saveAll()) { - if (!page->apply()) - return false; + return false; } if (m_currentPage) { @@ -229,3 +228,13 @@ bool PageContainer::prepareToClose() } return true; } + +bool PageContainer::saveAll() +{ + for (auto page : m_model->pages()) + { + if (!page->apply()) + return false; + } + return true; +} diff --git a/application/widgets/PageContainer.h b/application/widgets/PageContainer.h index ea9f8ce1..a05e74c4 100644 --- a/application/widgets/PageContainer.h +++ b/application/widgets/PageContainer.h @@ -46,6 +46,7 @@ public: * @return true if everything can be saved, false if there is something that requires attention */ bool prepareToClose(); + bool saveAll(); /* request close - used by individual pages */ bool requestClose() override