Commit a40117ed authored by Robby Stephenson's avatar Robby Stephenson
Browse files

Add feature to save collection template

Empty collection is saved as a template in $CONFIG/collection-templates.
Unit test for verifying validity of saved collection. Yet to do is to
add feature to remove saved templates.

BUG: 450043
FIXED-IN: 3.5
parent 672fd785
Pipeline #174170 passed with stage
in 4 minutes and 8 seconds
2022-05-09 Robby Stephenson <robby@periapsis.org>
* Added capability to save empty collection as new template (Bug 450043).
2022-04-27 Robby Stephenson <robby@periapsis.org>
* Updated ESRB rating for TheGamesDB data source.
......
......@@ -254,6 +254,23 @@ bool Document::saveDocument(const QUrl& url_, bool force_) {
return success;
}
bool Document::saveDocumentTemplate(const QUrl& url_, const QString& title_) {
Data::CollPtr collTemplate = CollectionFactory::collection(m_coll->type(), false /* no default fields */);
collTemplate->setTitle(title_);
// add the fields from the current collection
foreach(auto field, m_coll->fields()) {
collTemplate->addField(field);
}
foreach(auto filter, m_coll->filters()) {
collTemplate->addFilter(filter);
}
QScopedPointer<Export::Exporter> exporter(new Export::TellicoXMLExporter(collTemplate));
exporter->setURL(url_);
// since we already asked about overwriting the file, force the save
exporter->setOptions(exporter->options() | Export::ExportForce | Export::ExportComplete);
return exporter->exec();
}
bool Document::closeDocument() {
if(m_importer) {
m_importer->deleteLater();
......
......@@ -104,6 +104,7 @@ public:
* @return A boolean indicating success
*/
bool saveDocument(const QUrl& url, bool force = false);
bool saveDocumentTemplate(const QUrl& url, const QString& collTitle);
/**
* Closes the document, deleting the contents. The return value is presently always true.
*
......
......@@ -3,6 +3,7 @@
SET(gui_STAT_SRCS
boolfieldwidget.cpp
choicefieldwidget.cpp
collectiontemplatedialog.cpp
collectiontypecombo.cpp
combobox.cpp
countdelegate.cpp
......@@ -40,6 +41,11 @@ SET(gui_STAT_SRCS
urlfieldwidget.cpp
)
SET(gui_UI
collectiontemplatedialog.ui
)
qt_wrap_ui(gui_STAT_SRCS ${gui_UI})
add_library(gui STATIC ${gui_STAT_SRCS})
TARGET_LINK_LIBRARIES(gui
......@@ -53,6 +59,7 @@ TARGET_LINK_LIBRARIES(gui
KF5::XmlGui
KF5::I18n
KF5::TextWidgets
KF5::IconThemes
)
IF(USE_KHTML)
......
/***************************************************************************
Copyright (C) 2022 Robby Stephenson <robby@periapsis.org>
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License or (at your option) version 3 or any later version *
* accepted by the membership of KDE e.V. (or its successor approved *
* by the membership of KDE e.V.), which shall act as a proxy *
* defined in Section 14 of version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "collectiontemplatedialog.h"
#include "ui_collectiontemplatedialog.h"
#include <KLocalizedString>
#include <KIconButton>
using Tellico::CollectionTemplateDialog;
class CollectionTemplateDialog::Private {
public:
Private(CollectionTemplateDialog* parent) : q(parent) { }
void init();
CollectionTemplateDialog* const q;
QString templateName;
Ui::CollectionTemplateDialog ui;
};
void CollectionTemplateDialog::Private::init() {
ui.setupUi(q);
}
CollectionTemplateDialog::CollectionTemplateDialog(QWidget* parent_)
: QDialog(parent_)
, d(new Private(this)) {
d->init();
d->ui.labelName->setText(i18n("Template Name:"));
d->ui.labelComment->setText(i18n("Comment:"));
d->ui.labelIcon->setText(i18n("Icon:"));
d->ui.editIcon->setIconSize(KIconLoader::SizeMedium);
d->ui.editIcon->setIconType(KIconLoader::NoGroup, KIconLoader::Any);
setWindowTitle(i18n("Save As Collection Template"));
}
CollectionTemplateDialog::~CollectionTemplateDialog() = default;
QString CollectionTemplateDialog::templateName() const {
return d->ui.editName->text().trimmed();
}
QString CollectionTemplateDialog::templateComment() const {
return d->ui.editComment->text().trimmed();
}
QString CollectionTemplateDialog::templateIcon() const {
return d->ui.editIcon->icon();
}
/***************************************************************************
Copyright (C) 2022 Robby Stephenson <robby@periapsis.org>
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License or (at your option) version 3 or any later version *
* accepted by the membership of KDE e.V. (or its successor approved *
* by the membership of KDE e.V.), which shall act as a proxy *
* defined in Section 14 of version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef TELLICO_COLLECTIONTEMPLATEDIALOG_H
#define TELLICO_COLLECTIONTEMPLATEDIALOG_H
#include <QDialog>
#include <memory>
namespace Tellico {
/**
* @author Robby Stephenson
*/
class CollectionTemplateDialog : public QDialog {
Q_OBJECT
public:
explicit CollectionTemplateDialog(QWidget* parent = nullptr);
~CollectionTemplateDialog();
QString templateName() const;
QString templateComment() const;
QString templateIcon() const;
private:
class Private;
std::unique_ptr<Private> const d;
};
} // end namespace
#endif
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CollectionTemplateDialog</class>
<widget class="QDialog" name="CollectionTemplateDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>250</width>
<height>158</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="modal">
<bool>true</bool>
</property>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>242</width>
<height>152</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="labelName">
<property name="text">
<string>Template Name:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editName"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelComment">
<property name="text">
<string>Comment:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editComment"/>
</item>
<item row="0" column="1">
<widget class="KIconButton" name="editIcon">
<property name="iconSize">
<number>32</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelIcon">
<property name="text">
<string>Icon:</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>KIconButton</class>
<extends>QPushButton</extends>
<header>kiconbutton.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CollectionTemplateDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>144</x>
<y>263</y>
</hint>
<hint type="destinationlabel">
<x>141</x>
<y>205</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CollectionTemplateDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>229</x>
<y>244</y>
</hint>
<hint type="destinationlabel">
<x>255</x>
<y>209</y>
</hint>
</hints>
</connection>
</connections>
</ui>
......@@ -71,6 +71,7 @@
#include "gui/statusbar.h"
#include "gui/tabwidget.h"
#include "gui/dockwidget.h"
#include "gui/collectiontemplatedialog.h"
#include "utils/cursorsaver.h"
#include "utils/guiproxy.h"
#include "utils/tellico_utils.h"
......@@ -328,6 +329,13 @@ void MainWindow::initActions() {
m_fileSave->setToolTip(i18n("Save the document"));
action = KStandardAction::saveAs(this, SLOT(slotFileSaveAs()), actionCollection());
action->setToolTip(i18n("Save the document as a different file..."));
action = actionCollection()->addAction(QStringLiteral("file_save_template"),
this, SLOT(slotFileSaveAsTemplate()));
action->setText(i18n("Save As Template..."));
action->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as-template")));
action->setToolTip(i18n("Save as a collection template"));
action = KStandardAction::print(this, SLOT(slotFilePrint()), actionCollection());
action->setToolTip(i18n("Print the contents of the collection..."));
#ifdef USE_KHTML
......@@ -1211,6 +1219,26 @@ void MainWindow::slotFileNew(int type_) {
StatusBar::self()->clearStatus();
}
void MainWindow::slotFileNewByTemplate(const QString& collectionTemplate_) {
slotStatusMsg(i18n("Creating new collection..."));
// close the fields dialog
slotHideCollectionFieldsDialog();
if(m_editDialog->queryModified() && querySaveModified()) {
openURL(QUrl::fromLocalFile(collectionTemplate_));
Data::Document::self()->setURL(QUrl::fromLocalFile(i18n(Tellico::untitledFilename)));
Kernel::self()->resetHistory();
m_fileOpenRecent->setCurrentItem(-1);
slotEnableOpenedActions();
slotEnableModifiedActions(false);
m_newDocument = true;
ImageFactory::clean(false);
}
StatusBar::self()->clearStatus();
}
void MainWindow::slotFileOpen() {
slotStatusMsg(i18n("Opening file..."));
......@@ -1427,6 +1455,30 @@ bool MainWindow::fileSaveAs() {
return ret;
}
void MainWindow::slotFileSaveAsTemplate() {
QScopedPointer<CollectionTemplateDialog> dlg(new CollectionTemplateDialog(this));
if(dlg->exec() != QDialog::Accepted) {
return;
}
const QString templateName = dlg->templateName();
if(templateName.isEmpty()) {
return;
}
const QString baseName = Tellico::saveLocation(QStringLiteral("collection-templates/")) + templateName;
// first, save the collection template, which copies the collection fields and filters, but nothing else
const QString collFile = baseName + QLatin1String(".tc");
Data::Document::self()->saveDocumentTemplate(QUrl::fromLocalFile(collFile), templateName);
// next, save the template descriptions in a config file
const QString specFile = baseName + QLatin1String(".spec");
auto spec = KSharedConfig::openConfig(specFile, KConfig::SimpleConfig)->group(QString());
spec.writeEntry("Name", templateName);
spec.writeEntry("Comment", dlg->templateComment());
spec.writeEntry("Icon", dlg->templateIcon());
}
void MainWindow::slotFilePrint() {
doPrint(Print);
}
......@@ -2429,4 +2481,44 @@ void MainWindow::guiFactoryReset() {
guiFactory()->removeClient(this);
guiFactory()->reset();
guiFactory()->addClient(this);
// set up custom actions for collection templates, have to do this AFTER createGUI() or factory() reset
const QString actionListName = QStringLiteral("collection_template_list");
unplugActionList(actionListName);
QSignalMapper* collectionTemplateMapper = new QSignalMapper(this);
#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
void (QSignalMapper::* mappedString)(QString) = &QSignalMapper::mapped;
connect(collectionTemplateMapper, mappedString, this, &MainWindow::slotFileNewByTemplate);
#else
connect(collectionTemplateMapper, &QSignalMapper::mappedString, this, &MainWindow::slotFileNewByTemplate);
#endif
void (QAction::* triggeredBool)(bool) = &QAction::triggered;
void (QSignalMapper::* mapVoid)() = &QSignalMapper::map;
QList<QAction*> coll_actions;
const QStringList customCollections = Tellico::locateAllFiles(QStringLiteral("tellico/collection-templates/*.tc"));
if(!customCollections.isEmpty()) {
m_newCollectionMenu->addSeparator();
}
foreach(const QString& collectionFile, customCollections) {
QFileInfo info(collectionFile);
auto action = new QAction(info.completeBaseName(), actionCollection());
connect(action, triggeredBool, collectionTemplateMapper, mapVoid);
const QString specFile = info.canonicalPath() + QDir::separator() + info.completeBaseName() + QLatin1String(".spec");
if(QFileInfo::exists(specFile)) {
KConfig config(specFile, KConfig::SimpleConfig);
const KConfigGroup cg = config.group(QString());
action->setText(cg.readEntry("Name", info.completeBaseName()));
action->setToolTip(cg.readEntry("Comment"));
action->setIcon(QIcon::fromTheme(cg.readEntry("Icon"), QIcon::fromTheme(QStringLiteral("document-new"))));
} else {
myDebug() << "No spec file for" << info.completeBaseName();
action->setText(info.completeBaseName());
action->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
}
collectionTemplateMapper->setMapping(action, collectionFile);
coll_actions.append(action);
m_newCollectionMenu->addAction(action);
}
plugActionList(actionListName, coll_actions);
}
......@@ -150,6 +150,7 @@ public Q_SLOTS:
* @param type Type of collection to add
*/
void slotFileNew(int type);
void slotFileNewByTemplate(const QString& collectionTemplate);
/**
* Opens a file and loads it into the document
*/
......@@ -174,6 +175,7 @@ public Q_SLOTS:
* Saves a document by a new filename
*/
void slotFileSaveAs();
void slotFileSaveAsTemplate();
/**
* Prints the current document.
*/
......
<?xml version = '1.0'?>
<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd">
<kpartgui version="42" name="tellico">
<kpartgui version="43" name="tellico">
<MenuBar>
<Menu name="file">
<text>&amp;File</text>
<Menu append="new_merge" name="file_new_collection" >
<Menu append="new_merge" name="file_new_collection">
<text>&amp;New Collection</text>
<Action name="new_book_collection"/>
<Action name="new_bibtex_collection"/>
......@@ -19,7 +19,10 @@
<Action name="new_boardgame_collection"/>
<Action name="new_file_catalog"/>
<Action name="new_custom_collection"/>
<Separator/>
<ActionList name="collection_template_list"/>
</Menu>
<Action name="file_save_template" append="save_merge"/>
<Menu name="file_import">
<text context="@title:menu">&amp;Import</text>
<Action name="file_import_tellico"/>
......
/***************************************************************************
Copyright (C) 2015 Robby Stephenson <robby@periapsis.org>
Copyright (C) 2015-2022 Robby Stephenson <robby@periapsis.org>
***************************************************************************/
/***************************************************************************
......@@ -34,6 +34,7 @@
#include <QTest>
#include <QTemporaryDir>
#include <QTemporaryFile>
#include <QFile>
#include <QStandardPaths>
......@@ -141,3 +142,40 @@ void DocumentTest::testImageLocalDirectory() {
tempDir.remove();
QVERIFY(!QDir(tempDirName).exists());
}
void DocumentTest::testSaveTemplate() {
auto doc = Tellico::Data::Document::self();
QVERIFY(doc);
QVERIFY(doc->newDocument(Tellico::Data::Collection::Book));
auto coll = doc->collection();
Tellico::Data::EntryPtr entry1(new Tellico::Data::Entry(coll));
coll->addEntries(entry1);
entry1->setField(QStringLiteral("title"), QStringLiteral("new title"));
// modify a field too, to check that the template saves the modified field
auto field = coll->fieldByName(QStringLiteral("publisher"));
field->setTitle(QStringLiteral("batman"));
Tellico::FilterRule* rule1 = new Tellico::FilterRule(QStringLiteral("title"),
QStringLiteral("Star Wars"),
Tellico::FilterRule::FuncEquals);
Tellico::FilterPtr filter(new Tellico::Filter(Tellico::Filter::MatchAny));
filter->append(rule1);
coll->addFilter(filter);
QString templateName(QStringLiteral("my new template"));
QTemporaryFile templateFile(QStringLiteral("documenttest-template.XXXXXX.xml"));
QVERIFY(templateFile.open());
QUrl templateUrl = QUrl::fromLocalFile(templateFile.fileName());
QVERIFY(doc->saveDocumentTemplate(templateUrl, templateName));
QVERIFY(doc->openDocument(templateUrl));
auto new_coll = doc->collection();
QVERIFY(new_coll);
QCOMPARE(new_coll->title(), templateName);
QCOMPARE(new_coll->entryCount(), 0);
auto new_field = new_coll->fieldByName(QStringLiteral("publisher"));
QCOMPARE(new_field->title(), QStringLiteral("batman"));
QCOMPARE(new_coll->filters().count(), 1);
}
/***************************************************************************
Copyright (C) 2015 Robby Stephenson <robby@periapsis.org>
Copyright (C) 2015-2022 Robby Stephenson <robby@periapsis.org>
***************************************************************************/
/***************************************************************************
......@@ -35,6 +35,7 @@ private Q_SLOTS:
void cleanupTestCase();
void testImageLocalDirectory();
void testSaveTemplate();
};
#endif
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment