From 8f170eb83b5b1a966228409b3b282651893e2fbc Mon Sep 17 00:00:00 2001 From: Michael Mandl Date: Thu, 24 May 2018 20:35:16 +0200 Subject: [PATCH] refactored common test-model functions to base class --- source/CMakeLists.txt | 6 +- source/CheckableTest/CMakeLists.txt | 24 ++ source/CheckableTest/CheckableTest.cpp | 28 +++ source/CheckableTest/CheckableTest.h | 23 ++ source/CheckableTestModel/CMakeLists.txt | 25 +++ .../CheckableTestModel/CheckableTestModel.cpp | 179 +++++++++++++++ .../CheckableTestModel/CheckableTestModel.h | 40 ++++ source/Genus/CMakeLists.txt | 4 +- source/Genus/GenusModel.cpp | 209 +---------------- source/Genus/GenusModel.h | 36 +-- source/VerbEnd/CMakeLists.txt | 4 +- source/VerbEnd/VerbEndModel.cpp | 211 +----------------- source/VerbEnd/VerbEndModel.h | 37 +-- source/mainwindow.ui | 7 +- 14 files changed, 353 insertions(+), 480 deletions(-) create mode 100644 source/CheckableTest/CMakeLists.txt create mode 100644 source/CheckableTest/CheckableTest.cpp create mode 100644 source/CheckableTest/CheckableTest.h create mode 100644 source/CheckableTestModel/CMakeLists.txt create mode 100644 source/CheckableTestModel/CheckableTestModel.cpp create mode 100644 source/CheckableTestModel/CheckableTestModel.h diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index b75d007..3d236c7 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -35,7 +35,9 @@ target_include_directories(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} PRIVATE - CheckableItem + CheckableItem + CheckableTest + CheckableTestModel MetaData VerbEnd Genus @@ -43,6 +45,8 @@ target_link_libraries(${PROJECT_NAME} ) add_subdirectory(CheckableItem) +add_subdirectory(CheckableTest) +add_subdirectory(CheckableTestModel) add_subdirectory(MetaData) add_subdirectory(VerbEnd) diff --git a/source/CheckableTest/CMakeLists.txt b/source/CheckableTest/CMakeLists.txt new file mode 100644 index 0000000..574e82b --- /dev/null +++ b/source/CheckableTest/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.6) + +project(CheckableTest LANGUAGES CXX) + +find_package(Qt5Core REQUIRED) + +add_library(${PROJECT_NAME} + CheckableTest.cpp +) + +set_target_properties(${PROJECT_NAME} + PROPERTIES CXX_STANDARD 14 +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + CheckableItem + Qt5::Core +) diff --git a/source/CheckableTest/CheckableTest.cpp b/source/CheckableTest/CheckableTest.cpp new file mode 100644 index 0000000..0d0cbf5 --- /dev/null +++ b/source/CheckableTest/CheckableTest.cpp @@ -0,0 +1,28 @@ +#include "CheckableTest.h" + +CheckableTest::CheckableTest( + const char *name, std::initializer_list items) + : m_name(name) + , m_items(items) +{ +} + +size_t CheckableTest::size() const +{ + return m_items.size(); +} + +const QString &CheckableTest::name() const +{ + return m_name; +} + +const CheckableItems &CheckableTest::items() const +{ + return m_items; +} + +CheckableItems &CheckableTest::items() +{ + return m_items; +} diff --git a/source/CheckableTest/CheckableTest.h b/source/CheckableTest/CheckableTest.h new file mode 100644 index 0000000..424d44b --- /dev/null +++ b/source/CheckableTest/CheckableTest.h @@ -0,0 +1,23 @@ +#pragma once + +#include "CheckableItems.h" + +#include + +class CheckableTest +{ +private: + CheckableItems m_items; + QString m_name; + +public: + CheckableTest(const char *name, std::initializer_list items); + + size_t size() const; + const QString &name() const; + const CheckableItems &items() const; + CheckableItems &items(); +}; + +using CheckableTests = std::vector; + diff --git a/source/CheckableTestModel/CMakeLists.txt b/source/CheckableTestModel/CMakeLists.txt new file mode 100644 index 0000000..2ce7c3b --- /dev/null +++ b/source/CheckableTestModel/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.6) + +project(CheckableTestModel LANGUAGES CXX) + +find_package(Qt5Core REQUIRED) + +add_library(${PROJECT_NAME} + CheckableTestModel.cpp +) + +set_target_properties(${PROJECT_NAME} + PROPERTIES CXX_STANDARD 14 +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + CheckableItem + CheckableTest + Qt5::Core +) diff --git a/source/CheckableTestModel/CheckableTestModel.cpp b/source/CheckableTestModel/CheckableTestModel.cpp new file mode 100644 index 0000000..f8df7ef --- /dev/null +++ b/source/CheckableTestModel/CheckableTestModel.cpp @@ -0,0 +1,179 @@ +#include "CheckableTestModel.h" + +#include +#include + +CheckableTestModel::CheckableTestModel(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +int CheckableTestModel::rowCount(const QModelIndex &parent) const +{ + return m_tests.size(); +} + +int CheckableTestModel::columnCount(const QModelIndex &parent) const +{ + int columnCount = 0; + + for (const auto &test : m_tests) + { + columnCount = std::max(columnCount, test.size()); + } + + return columnCount; +} + +QVariant CheckableTestModel::data(const QModelIndex &index, int role) const +{ + if (!isValidIndex(index)) + { + return {}; + } + + try + { + auto &item = getItem(index); + + if (role == Qt::DisplayRole) + { + return item.getText().c_str(); + } + + if (role == Qt::CheckStateRole) + { + return item.isChecked() ? Qt::Checked : Qt::Unchecked; + } + } + catch (std::runtime_error &e) + { + qDebug() << "CheckableTestModel::data" << index << e.what(); + } + + return {}; +} + +Qt::ItemFlags CheckableTestModel::flags(const QModelIndex &index) const +{ + if (isValidIndex(index)) + { + return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; + } + + return Qt::NoItemFlags; +} + +bool CheckableTestModel::setData( + const QModelIndex &index, const QVariant &value, int role) +{ + if (!isValidIndex(index)) + { + return false; + } + + try + { + if (role == Qt::CheckStateRole) + { + auto &item = getItem(index); + item.setState(value.toBool()); + return true; + } + } + catch (std::runtime_error &e) + { + qDebug() << "CheckableTestModel::setData" << index << e.what(); + } + + return false; +} + +QVariant CheckableTestModel::headerData( + int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::DisplayRole && orientation == Qt::Vertical) + { + if (section < m_tests.size()) + { + return m_tests.at(section).name(); + } + } + + return {}; +} + +void CheckableTestModel::write(QJsonObject &json) const +{ + for (const auto &test : m_tests) + { + QJsonArray testData; + test.items().write(testData); + json[test.name()] = testData; + } +} + +void CheckableTestModel::read(const QJsonObject &json) +{ + for (auto &test : m_tests) + { + auto testData = json[test.name()]; + if (testData.isArray()) + { + test.items().read(testData.toArray()); + } + } +} + +bool CheckableTestModel::isValidIndex(const QModelIndex &index) const +{ + if (index.row() < m_tests.size()) + { + return index.column() < m_tests.at(index.row()).size(); + } + + return false; +} + +CheckableItems &CheckableTestModel::getItems(const QModelIndex &index) +{ + if (index.row() < m_tests.size()) + { + return m_tests.at(index.row()).items(); + } + + throw std::runtime_error("invalid index"); +} + +const CheckableItems &CheckableTestModel::getItems( + const QModelIndex &index) const +{ + if (index.row() < m_tests.size()) + { + return m_tests.at(index.row()).items(); + } + + throw std::runtime_error("invalid index"); +} + +CheckableItem &CheckableTestModel::getItem(const QModelIndex &index) +{ + auto &items = getItems(index); + if (index.column() < items.size()) + { + return items.at(index.column()); + } + + throw std::runtime_error("invalid index"); +} + +const CheckableItem &CheckableTestModel::getItem(const QModelIndex &index) const +{ + auto &items = getItems(index); + if (index.column() < items.size()) + { + return items.at(index.column()); + } + + throw std::runtime_error("invalid index"); +} diff --git a/source/CheckableTestModel/CheckableTestModel.h b/source/CheckableTestModel/CheckableTestModel.h new file mode 100644 index 0000000..9061a27 --- /dev/null +++ b/source/CheckableTestModel/CheckableTestModel.h @@ -0,0 +1,40 @@ +#pragma once + +#include "CheckableTest.h" +#include + +class CheckableTestModel : public QAbstractTableModel +{ + Q_OBJECT + +protected: + CheckableTests m_tests; + +public: + CheckableTestModel(QObject *parent); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data( + const QModelIndex &index, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) override; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + + void write(QJsonObject &json) const; + void read(const QJsonObject &json); + +private: + bool isValidIndex(const QModelIndex &index) const; + + CheckableItems &getItems(const QModelIndex &index); + const CheckableItems &getItems(const QModelIndex &index) const; + + CheckableItem &getItem(const QModelIndex &index); + const CheckableItem &getItem(const QModelIndex &index) const; +}; + diff --git a/source/Genus/CMakeLists.txt b/source/Genus/CMakeLists.txt index cf7dcf1..b27ef6d 100644 --- a/source/Genus/CMakeLists.txt +++ b/source/Genus/CMakeLists.txt @@ -27,6 +27,8 @@ target_include_directories(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} PRIVATE - CheckableItem + CheckableItem + CheckableTest + CheckableTestModel Qt5::Widgets ) diff --git a/source/Genus/GenusModel.cpp b/source/Genus/GenusModel.cpp index 7d2321e..f1a284f 100644 --- a/source/Genus/GenusModel.cpp +++ b/source/Genus/GenusModel.cpp @@ -1,208 +1,11 @@ #include "GenusModel.h" -#include -#include - GenusModel::GenusModel(QObject *parent) - : QAbstractTableModel(parent) + : CheckableTestModel(parent) { -} - -int GenusModel::rowCount(const QModelIndex &parent) const -{ - return 3; -} - -int GenusModel::columnCount(const QModelIndex &parent) const -{ - return 8; -} - -QVariant GenusModel::data(const QModelIndex &index, int role) const -{ - if (!isValidIndex(index)) - { - return {}; - } - - try - { - auto &item = getItem(index); - - if (role == Qt::DisplayRole) - { - return item.getText().c_str(); - } - - if (role == Qt::CheckStateRole) - { - return item.isChecked() ? Qt::Checked : Qt::Unchecked; - } - } - catch (std::runtime_error &e) - { - qDebug() << "GenusModel::data" << index << e.what(); - } - - return {}; -} - -Qt::ItemFlags GenusModel::flags(const QModelIndex &index) const -{ - if (isValidIndex(index)) - { - return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; - } - - return Qt::NoItemFlags; -} - -bool GenusModel::setData( - const QModelIndex &index, const QVariant &value, int role) -{ - if (!isValidIndex(index)) - { - return false; - } - - try - { - if (role == Qt::CheckStateRole) - { - auto &item = getItem(index); - item.setState(value.toBool()); - return true; - } - } - catch (std::runtime_error &e) - { - qDebug() << "GenusModel::setData" << index << e.what(); - } - - return false; -} - -QVariant GenusModel::headerData( - int section, Qt::Orientation orientation, int role) const -{ - if (role == Qt::DisplayRole && orientation == Qt::Vertical) - { - switch (section) - { - case 0: - return "Tiere"; - case 1: - return "Futter"; - case 2: - return "Zirkus"; - default: - return {}; - } - } - - return {}; -} - -void GenusModel::write(QJsonObject &json) const -{ - QJsonArray tiere; - m_tiere.write(tiere); - json["Tiere"] = tiere; - - QJsonArray futter; - m_futter.write(futter); - json["Futter"] = futter; - - QJsonArray zirkus; - m_zirkus.write(zirkus); - json["Zirkus"] = zirkus; -} - -void GenusModel::read(const QJsonObject &json) -{ - if (json["Tiere"].isArray()) - { - m_tiere.read(json["Tiere"].toArray()); - } - - if (json["Futter"].isArray()) - { - m_futter.read(json["Futter"].toArray()); - } - - if (json["Zirkus"].isArray()) - { - m_zirkus.read(json["Zirkus"].toArray()); - } -} - -bool GenusModel::isValidIndex(const QModelIndex &index) const -{ - switch (index.row()) - { - case 0: - return index.column() < m_tiere.size(); - case 1: - return index.column() < m_futter.size(); - case 2: - return index.column() < m_zirkus.size(); - default: - return false; - } -} - -CheckableItems &GenusModel::getItems(const QModelIndex &index) -{ - switch (index.row()) - { - case 0: - return m_tiere; - case 1: - return m_futter; - case 2: - return m_zirkus; - default: - break; - } - - throw std::runtime_error("invalid index"); -} - -const CheckableItems &GenusModel::getItems(const QModelIndex &index) const -{ - switch (index.row()) - { - case 0: - return m_tiere; - case 1: - return m_futter; - case 2: - return m_zirkus; - default: - break; - } - - throw std::runtime_error("invalid index"); -} - -CheckableItem &GenusModel::getItem(const QModelIndex &index) -{ - auto &items = getItems(index); - if (index.column() < items.size()) - { - return items.at(index.column()); - } - - throw std::runtime_error("invalid index"); -} - -const CheckableItem &GenusModel::getItem(const QModelIndex &index) const -{ - auto &items = getItems(index); - if (index.column() < items.size()) - { - return items.at(index.column()); - } - - throw std::runtime_error("invalid index"); + m_tests = { { "Tiere", { "Tiger", "Bär", "Katze", "Pferd", "Gans", + "Elefant", "Katze", "Hund" } }, + { "Futter", { "Salat", "Fleisch", "Knocken", "Banane", "Apfel", "Möhre", + "Honig", "Zucker" } }, + { "Zirkus", { "Kiste", "Holz", "Vorhang", "Baum" } } }; } diff --git a/source/Genus/GenusModel.h b/source/Genus/GenusModel.h index bceab15..24f297a 100644 --- a/source/Genus/GenusModel.h +++ b/source/Genus/GenusModel.h @@ -1,43 +1,11 @@ #pragma once -#include "CheckableItem.h" -#include "CheckableItems.h" +#include "CheckableTestModel.h" -#include - -class GenusModel : public QAbstractTableModel +class GenusModel : public CheckableTestModel { Q_OBJECT -private: - CheckableItems m_tiere = { "Tiger", "Bär", "Katze", "Pferd", "Gans", - "Elefant", "Katze", "Hund" }; - CheckableItems m_futter = { "Salat", "Fleisch", "Knocken", "Banane", - "Apfel", "Möhre", "Honig", "Zucker" }; - CheckableItems m_zirkus = { "Kiste", "Holz", "Vorhang", "Baum" }; - public: GenusModel(QObject *parent); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data( - const QModelIndex &index, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool setData(const QModelIndex &index, const QVariant &value, - int role = Qt::EditRole) override; - - QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const override; - - void write(QJsonObject &json) const; - void read(const QJsonObject &json); - -private: - bool isValidIndex(const QModelIndex &index) const; - - CheckableItems &getItems(const QModelIndex &index); - const CheckableItems &getItems(const QModelIndex &index) const; - - CheckableItem &getItem(const QModelIndex &index); - const CheckableItem &getItem(const QModelIndex &index) const; }; diff --git a/source/VerbEnd/CMakeLists.txt b/source/VerbEnd/CMakeLists.txt index 06bb136..d9d91ff 100644 --- a/source/VerbEnd/CMakeLists.txt +++ b/source/VerbEnd/CMakeLists.txt @@ -27,6 +27,8 @@ target_include_directories(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} PRIVATE - CheckableItem + CheckableItem + CheckableTest + CheckableTestModel Qt5::Widgets ) diff --git a/source/VerbEnd/VerbEndModel.cpp b/source/VerbEnd/VerbEndModel.cpp index 38175e5..8793873 100644 --- a/source/VerbEnd/VerbEndModel.cpp +++ b/source/VerbEnd/VerbEndModel.cpp @@ -1,208 +1,13 @@ #include "VerbEndModel.h" -#include -#include - VerbEndModel::VerbEndModel(QObject *parent) - : QAbstractTableModel(parent) + : CheckableTestModel(parent) { -} - -int VerbEndModel::rowCount(const QModelIndex &parent) const -{ - return 3; -} - -int VerbEndModel::columnCount(const QModelIndex &parent) const -{ - return 7; -} - -QVariant VerbEndModel::data(const QModelIndex &index, int role) const -{ - if (!isValidIndex(index)) - { - return {}; - } - - try - { - auto &item = getItem(index); - - if (role == Qt::DisplayRole) - { - return item.getText().c_str(); - } - - if (role == Qt::CheckStateRole) - { - return item.isChecked() ? Qt::Checked : Qt::Unchecked; - } - } - catch (std::runtime_error &e) - { - qDebug() << "VerbEndModel::data" << index << e.what(); - } - - return {}; -} - -Qt::ItemFlags VerbEndModel::flags(const QModelIndex &index) const -{ - if (isValidIndex(index)) - { - return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; - } - - return Qt::NoItemFlags; -} - -bool VerbEndModel::setData( - const QModelIndex &index, const QVariant &value, int role) -{ - if (!isValidIndex(index)) - { - return false; - } - - try - { - if (role == Qt::CheckStateRole) - { - auto &item = getItem(index); - item.setState(value.toBool()); - return true; - } - } - catch (std::runtime_error &e) - { - qDebug() << "VerbEndModel::setData" << index << e.what(); - } - - return false; -} - -QVariant VerbEndModel::headerData( - int section, Qt::Orientation orientation, int role) const -{ - if (role == Qt::DisplayRole && orientation == Qt::Vertical) - { - switch (section) - { - case 0: - return "Telefonat"; - case 1: - return "Zaubertrick"; - case 2: - return "Zauberregel"; - default: - return {}; - } - } - - return {}; -} - -void VerbEndModel::write(QJsonObject &json) const -{ - QJsonArray telefonat; - m_telefonat.write(telefonat); - json["Telefonat"] = telefonat; - - QJsonArray zaubertrick; - m_zaubertrick.write(zaubertrick); - json["Zaubertrick"] = zaubertrick; - - QJsonArray zauberregel; - m_zauberregel.write(zauberregel); - json["Zauberregel"] = zauberregel; -} - -void VerbEndModel::read(const QJsonObject &json) -{ - if (json["Telefonat"].isArray()) - { - m_telefonat.read(json["Telefonat"].toArray()); - } - - if (json["Zaubertrick"].isArray()) - { - m_zaubertrick.read(json["Zaubertrick"].toArray()); - } - - if (json["Zauberregel"].isArray()) - { - m_zauberregel.read(json["Zauberregel"].toArray()); - } -} - -bool VerbEndModel::isValidIndex(const QModelIndex &index) const -{ - switch (index.row()) - { - case 0: - return index.column() < m_telefonat.size(); - case 1: - return index.column() < m_zaubertrick.size(); - case 2: - return index.column() < m_zauberregel.size(); - default: - return false; - } -} - -CheckableItems &VerbEndModel::getItems(const QModelIndex &index) -{ - switch (index.row()) - { - case 0: - return m_telefonat; - case 1: - return m_zaubertrick; - case 2: - return m_zauberregel; - default: - break; - } - - throw std::runtime_error("invalid index"); -} - -const CheckableItems &VerbEndModel::getItems(const QModelIndex &index) const -{ - switch (index.row()) - { - case 0: - return m_telefonat; - case 1: - return m_zaubertrick; - case 2: - return m_zauberregel; - default: - break; - } - - throw std::runtime_error("invalid index"); -} - -CheckableItem &VerbEndModel::getItem(const QModelIndex &index) -{ - auto &items = getItems(index); - if (index.column() < items.size()) - { - return items.at(index.column()); - } - - throw std::runtime_error("invalid index"); -} - -const CheckableItem &VerbEndModel::getItem(const QModelIndex &index) const -{ - auto &items = getItems(index); - if (index.column() < items.size()) - { - return items.at(index.column()); - } - - throw std::runtime_error("invalid index"); + m_tests = { { "Telefonat", + { "Kausal (1)", "Kausal (2)", "Relativ", "Kausal (3)", + "Final", "Temporal (1)", "Temporal (2)" } }, + { "Zaubertrick", { "Relativ", "Final (1)", "Kausal (1)", "Final (2)", + "Temporal (1)", "Kausal (2)", "Temporal (2)" } }, + { "Zauberregel", { "Temporal (1)", "Kausal", "Final", "Relativ (1)", + "Temporal (2)", "Relativ (2)" } } }; } diff --git a/source/VerbEnd/VerbEndModel.h b/source/VerbEnd/VerbEndModel.h index e73867f..42ac7d4 100644 --- a/source/VerbEnd/VerbEndModel.h +++ b/source/VerbEnd/VerbEndModel.h @@ -1,44 +1,11 @@ #pragma once -#include "CheckableItem.h" -#include "CheckableItems.h" +#include "CheckableTestModel.h" -#include - -class VerbEndModel : public QAbstractTableModel +class VerbEndModel : public CheckableTestModel { Q_OBJECT -private: - CheckableItems m_telefonat = { "Kausal (1)", "Kausal (2)", "Relativ", - "Kausal (3)", "Final", "Temporal (1)", "Temporal (2)" }; - CheckableItems m_zaubertrick = { "Relativ", "Final (1)", "Kausal (1)", - "Final (2)", "Temporal (1)", "Kausal (2)", "Temporal (2)" }; - CheckableItems m_zauberregel = { "Temporal (1)", "Kausal", "Final", - "Relativ (1)", "Temporal (2)", "Relativ (2)" }; - public: VerbEndModel(QObject *parent); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data( - const QModelIndex &index, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool setData(const QModelIndex &index, const QVariant &value, - int role = Qt::EditRole) override; - - QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const override; - - void write(QJsonObject &json) const; - void read(const QJsonObject &json); - -private: - bool isValidIndex(const QModelIndex &index) const; - - CheckableItems &getItems(const QModelIndex &index); - const CheckableItems &getItems(const QModelIndex &index) const; - - CheckableItem &getItem(const QModelIndex &index); - const CheckableItem &getItem(const QModelIndex &index) const; }; diff --git a/source/mainwindow.ui b/source/mainwindow.ui index 3c7b8a9..fa1fd31 100644 --- a/source/mainwindow.ui +++ b/source/mainwindow.ui @@ -18,7 +18,7 @@ - 0 + 2 @@ -60,7 +60,7 @@ 0 0 905 - 17 + 19 @@ -101,6 +101,9 @@ Save as... + + Ctrl+Shift+S +