File indexing completed on 2024-04-28 05:08:25

0001 /***************************************************************************
0002     Copyright (C) 2001-2020 Robby Stephenson <robby@periapsis.org>
0003     Copyright (C) 2011 Pedro Miguel Carvalho <kde@pmc.com.pt>
0004  ***************************************************************************/
0005 
0006 /***************************************************************************
0007  *                                                                         *
0008  *   This program is free software; you can redistribute it and/or         *
0009  *   modify it under the terms of the GNU General Public License as        *
0010  *   published by the Free Software Foundation; either version 2 of        *
0011  *   the License or (at your option) version 3 or any later version        *
0012  *   accepted by the membership of KDE e.V. (or its successor approved     *
0013  *   by the membership of KDE e.V.), which shall act as a proxy            *
0014  *   defined in Section 14 of version 3 of the license.                     *
0015  *                                                                         *
0016  *   This program is distributed in the hope that it will be useful,       *
0017  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0018  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0019  *   GNU General Public License for more details.                          *
0020  *                                                                         *
0021  *   You should have received a copy of the GNU General Public License     *
0022  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0023  *                                                                         *
0024  ***************************************************************************/
0025 
0026 #include "mainwindow.h"
0027 #include "tellico_kernel.h"
0028 #include "document.h"
0029 #include "detailedlistview.h"
0030 #include "entryeditdialog.h"
0031 #include "groupview.h"
0032 #include "viewstack.h"
0033 #include "collection.h"
0034 #include "collectionfactory.h"
0035 #include "entry.h"
0036 #include "configdialog.h"
0037 #include "filter.h"
0038 #include "filterdialog.h"
0039 #include "collectionfieldsdialog.h"
0040 #include "controller.h"
0041 #include "importdialog.h"
0042 #include "exportdialog.h"
0043 #include "core/filehandler.h" // needed so static mainWindow variable can be set
0044 #include "core/logger.h"
0045 #include "printhandler.h"
0046 #include "entryview.h"
0047 #include "entryiconview.h"
0048 #include "images/imagefactory.h" // needed so tmp files can get cleaned
0049 #include "collections/collectioninitializer.h"
0050 #include "collections/bibtexcollection.h" // needed for bibtex string macro dialog
0051 #include "utils/bibtexhandler.h" // needed for bibtex options
0052 #include "utils/datafileregistry.h"
0053 #include "fetchdialog.h"
0054 #include "reportdialog.h"
0055 #include "bibtexkeydialog.h"
0056 #include "core/tellico_strings.h"
0057 #include "filterview.h"
0058 #include "loanview.h"
0059 #include "fetch/fetchmanager.h"
0060 #include "fetch/fetcherinitializer.h"
0061 #include "cite/actionmanager.h"
0062 #include "config/tellico_config.h"
0063 #include "core/netaccess.h"
0064 #include "dbusinterface.h"
0065 #include "models/models.h"
0066 #include "models/entryiconmodel.h"
0067 #include "models/entryselectionmodel.h"
0068 #include "newstuff/manager.h"
0069 #include "gui/drophandler.h"
0070 #include "gui/stringmapdialog.h"
0071 #include "gui/lineedit.h"
0072 #include "gui/statusbar.h"
0073 #include "gui/tabwidget.h"
0074 #include "gui/dockwidget.h"
0075 #include "gui/collectiontemplatedialog.h"
0076 #include "utils/cursorsaver.h"
0077 #include "utils/guiproxy.h"
0078 #include "utils/tellico_utils.h"
0079 #include "tellico_debug.h"
0080 
0081 #include <KComboBox>
0082 #include <KToolBar>
0083 #include <KLocalizedString>
0084 #include <KConfig>
0085 #include <KStandardAction>
0086 #include <KWindowConfig>
0087 #include <KMessageBox>
0088 #include <KTipDialog>
0089 #include <KRecentDocument>
0090 #include <KRecentDirs>
0091 #include <KEditToolBar>
0092 #include <KShortcutsDialog>
0093 #include <KRecentFilesAction>
0094 #include <KToggleAction>
0095 #include <KActionCollection>
0096 #include <KActionMenu>
0097 #include <KFileWidget>
0098 #include <KDualAction>
0099 #include <KXMLGUIFactory>
0100 #include <KAboutData>
0101 #include <kwidgetsaddons_version.h>
0102 
0103 #include <QApplication>
0104 #include <QUndoStack>
0105 #include <QAction>
0106 #include <QSignalMapper>
0107 #include <QTimer>
0108 #include <QMetaObject> // needed for copy, cut, paste slots
0109 #include <QMimeDatabase>
0110 #include <QMimeType>
0111 #include <QMenuBar>
0112 #include <QFileDialog>
0113 #include <QMetaMethod>
0114 #include <QVBoxLayout>
0115 #include <QTextEdit>
0116 
0117 namespace {
0118   static const int MAX_IMAGES_WARN_PERFORMANCE = 200;
0119 
0120 QIcon mimeIcon(const char* s) {
0121   QMimeDatabase db;
0122   QMimeType ptr = db.mimeTypeForName(QLatin1String(s));
0123   if(!ptr.isValid()) {
0124     myDebug() << "*** no icon for" << s;
0125   }
0126   return ptr.isValid() ? QIcon::fromTheme(ptr.iconName()) : QIcon();
0127 }
0128 
0129 QIcon mimeIcon(const char* s1, const char* s2) {
0130   QMimeDatabase db;
0131   QMimeType ptr = db.mimeTypeForName(QLatin1String(s1));
0132   if(!ptr.isValid()) {
0133     ptr = db.mimeTypeForName(QLatin1String(s2));
0134     if(!ptr.isValid()) {
0135       myDebug() << "*** no icon for" << s1 << "or" << s2;
0136     }
0137   }
0138   return ptr.isValid() ? QIcon::fromTheme(ptr.iconName()) : QIcon();
0139 }
0140 
0141 }
0142 
0143 using namespace Tellico;
0144 using Tellico::MainWindow;
0145 
0146 MainWindow::MainWindow(QWidget* parent_/*=0*/) : KXmlGuiWindow(parent_),
0147     m_updateAll(nullptr),
0148     m_statusBar(nullptr),
0149     m_editDialog(nullptr),
0150     m_groupView(nullptr),
0151     m_filterView(nullptr),
0152     m_loanView(nullptr),
0153     m_configDlg(nullptr),
0154     m_filterDlg(nullptr),
0155     m_collFieldsDlg(nullptr),
0156     m_stringMacroDlg(nullptr),
0157     m_bibtexKeyDlg(nullptr),
0158     m_fetchDlg(nullptr),
0159     m_reportDlg(nullptr),
0160     m_queuedFilters(0),
0161     m_initialized(false),
0162     m_newDocument(true),
0163     m_dontQueueFilter(false),
0164     m_savingImageLocationChange(false) {
0165 
0166   Controller::init(this); // the only time this is ever called!
0167   // has to be after controller init
0168   Kernel::init(this); // the only time this is ever called!
0169   GUI::Proxy::setMainWidget(this);
0170 
0171   setWindowIcon(QIcon::fromTheme(QStringLiteral("tellico"),
0172                                  QIcon(QLatin1String(":/icons/tellico"))));
0173 
0174   // initialize the status bar and progress bar
0175   initStatusBar();
0176 
0177   // initialize all the collection types
0178   // which must be done before the document is created
0179   CollectionInitializer initCollections;
0180   // register all the fetcher types
0181   Fetch::FetcherInitializer initFetchers;
0182 
0183   // create a document, which also creates an empty book collection
0184   // must be done before the different widgets are created
0185   initDocument();
0186 
0187   // set up all the actions, some connect to the document, so this must be after initDocument()
0188   initActions();
0189 
0190   // create the different widgets in the view, some widgets connect to actions, so must be after initActions()
0191   initView();
0192 
0193   // The edit dialog is not created until after the main window is initialized, so it can be a child.
0194   // So don't make any connections, don't read options for it until initFileOpen
0195   readOptions();
0196 
0197   setAcceptDrops(true);
0198   DropHandler* drophandler = new DropHandler(this);
0199   installEventFilter(drophandler);
0200 
0201   new ApplicationInterface(this);
0202   new CollectionInterface(this);
0203 
0204   MARK_LINE;
0205   QTimer::singleShot(0, this, &MainWindow::slotInit);
0206 }
0207 
0208 MainWindow::~MainWindow() {
0209   qDeleteAll(m_fetchActions);
0210   m_fetchActions.clear();
0211   // when closing the mainwindow, immediately after running Tellico, often there was a long pause
0212   // before the application eventually quit, something related to polling on eventfd, I don't
0213   // know what. So when closing the window, make sure to immediately quit the application
0214   QTimer::singleShot(0, qApp, &QCoreApplication::quit);
0215 }
0216 
0217 void MainWindow::slotInit() {
0218   // if the edit dialog exists, we know we've already called this function
0219   if(m_editDialog) {
0220     return;
0221   }
0222   MARK;
0223 
0224   m_editDialog = new EntryEditDialog(this);
0225   Controller::self()->addObserver(m_editDialog);
0226 
0227   m_toggleEntryEditor->setChecked(Config::showEditWidget());
0228   slotToggleEntryEditor();
0229   m_lockLayout->setActive(Config::lockLayout());
0230 
0231   initConnections();
0232   connect(ImageFactory::self(), &ImageFactory::imageLocationMismatch,
0233           this, &MainWindow::slotImageLocationMismatch);
0234   // Init DBUS for new stuff manager
0235   NewStuff::Manager::self();
0236 }
0237 
0238 void MainWindow::initStatusBar() {
0239   MARK;
0240   m_statusBar = new Tellico::StatusBar(this);
0241   setStatusBar(m_statusBar);
0242 }
0243 
0244 void MainWindow::initActions() {
0245   MARK;
0246   /*************************************************
0247    * File->New menu
0248    *************************************************/
0249   QSignalMapper* collectionMapper = new QSignalMapper(this);
0250 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0251   void (QSignalMapper::* mappedInt)(int) = &QSignalMapper::mapped;
0252   connect(collectionMapper, mappedInt, this, &MainWindow::slotFileNew);
0253 #else
0254   connect(collectionMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotFileNew);
0255 #endif
0256 
0257   m_newCollectionMenu = new KActionMenu(i18n("New Collection"), this);
0258   m_newCollectionMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
0259   m_newCollectionMenu->setToolTip(i18n("Create a new collection"));
0260 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
0261   m_newCollectionMenu->setPopupMode(QToolButton::InstantPopup);
0262 #else
0263   m_newCollectionMenu->setDelayed(false);
0264 #endif
0265   actionCollection()->addAction(QStringLiteral("file_new_collection"), m_newCollectionMenu);
0266 
0267   QAction* action;
0268 
0269   void (QSignalMapper::* mapVoid)() = &QSignalMapper::map;
0270 #define COLL_ACTION(TYPE, NAME, TEXT, TIP, ICON) \
0271   action = actionCollection()->addAction(QStringLiteral(NAME), collectionMapper, mapVoid); \
0272   action->setText(TEXT); \
0273   action->setToolTip(TIP); \
0274   action->setIcon(QIcon(QStringLiteral(":/icons/" ICON))); \
0275   m_newCollectionMenu->addAction(action); \
0276   collectionMapper->setMapping(action, Data::Collection::TYPE);
0277 
0278   COLL_ACTION(Book, "new_book_collection", i18n("New &Book Collection"),
0279               i18n("Create a new book collection"), "book");
0280 
0281   COLL_ACTION(Bibtex, "new_bibtex_collection", i18n("New B&ibliography"),
0282               i18n("Create a new bibtex bibliography"), "bibtex");
0283 
0284   COLL_ACTION(ComicBook, "new_comic_book_collection", i18n("New &Comic Book Collection"),
0285               i18n("Create a new comic book collection"), "comic");
0286 
0287   COLL_ACTION(Video, "new_video_collection", i18n("New &Video Collection"),
0288               i18n("Create a new video collection"), "video");
0289 
0290   COLL_ACTION(Album, "new_music_collection", i18n("New &Music Collection"),
0291               i18n("Create a new music collection"), "album");
0292 
0293   COLL_ACTION(Coin, "new_coin_collection", i18n("New C&oin Collection"),
0294               i18n("Create a new coin collection"), "coin");
0295 
0296   COLL_ACTION(Stamp, "new_stamp_collection", i18n("New &Stamp Collection"),
0297               i18n("Create a new stamp collection"), "stamp");
0298 
0299   COLL_ACTION(Card, "new_card_collection", i18n("New C&ard Collection"),
0300               i18n("Create a new trading card collection"), "card");
0301 
0302   COLL_ACTION(Wine, "new_wine_collection", i18n("New &Wine Collection"),
0303               i18n("Create a new wine collection"), "wine");
0304 
0305   COLL_ACTION(Game, "new_game_collection", i18n("New Video &Game Collection"),
0306               i18n("Create a new video game collection"), "game");
0307 
0308   COLL_ACTION(BoardGame, "new_boardgame_collection", i18n("New Boa&rd Game Collection"),
0309               i18n("Create a new board game collection"), "boardgame");
0310 
0311   COLL_ACTION(File, "new_file_catalog", i18n("New &File Catalog"),
0312               i18n("Create a new file catalog"), "file");
0313 
0314   action = actionCollection()->addAction(QStringLiteral("new_custom_collection"), collectionMapper, mapVoid);
0315   action->setText(i18n("New C&ustom Collection"));
0316   action->setToolTip(i18n("Create a new custom collection"));
0317   action->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
0318   m_newCollectionMenu->addAction(action);
0319   collectionMapper->setMapping(action, Data::Collection::Base);
0320 
0321 #undef COLL_ACTION
0322 
0323   /*************************************************
0324    * File menu
0325    *************************************************/
0326   action = KStandardAction::open(this, SLOT(slotFileOpen()), actionCollection());
0327   action->setToolTip(i18n("Open an existing document"));
0328   m_fileOpenRecent = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(const QUrl&)), actionCollection());
0329   m_fileOpenRecent->setToolTip(i18n("Open a recently used file"));
0330   m_fileSave = KStandardAction::save(this, SLOT(slotFileSave()), actionCollection());
0331   m_fileSave->setToolTip(i18n("Save the document"));
0332   action = KStandardAction::saveAs(this, SLOT(slotFileSaveAs()), actionCollection());
0333   action->setToolTip(i18n("Save the document as a different file..."));
0334 
0335   action = actionCollection()->addAction(QStringLiteral("file_save_template"),
0336                                          this, SLOT(slotFileSaveAsTemplate()));
0337   action->setText(i18n("Save As Template..."));
0338   action->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as-template")));
0339   action->setToolTip(i18n("Save as a collection template"));
0340 
0341   action = KStandardAction::print(this, SLOT(slotFilePrint()), actionCollection());
0342   action->setToolTip(i18n("Print the contents of the collection..."));
0343 #ifdef USE_KHTML
0344   {
0345     KHTMLPart w;
0346     // KHTMLPart printing was broken in KDE until KHTML 5.16
0347     const QString version =  w.componentData().version();
0348     const uint major = version.section(QLatin1Char('.'), 0, 0).toUInt();
0349     const uint minor = version.section(QLatin1Char('.'), 1, 1).toUInt();
0350     if(major == 5 && minor < 16) {
0351       myWarning() << "Printing is broken for KDE Frameworks < 5.16. Please upgrade";
0352       action->setEnabled(false);
0353     }
0354   }
0355 #else
0356   // print preview is only available with QWebEngine
0357   action = KStandardAction::printPreview(this, SLOT(slotFilePrintPreview()), actionCollection());
0358   action->setToolTip(i18n("Preview the contents of the collection..."));
0359 #endif
0360 
0361   action = KStandardAction::quit(this, SLOT(slotFileQuit()), actionCollection());
0362   action->setToolTip(i18n("Quit the application"));
0363 
0364 /**************** Import Menu ***************************/
0365 
0366   QSignalMapper* importMapper = new QSignalMapper(this);
0367 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0368   connect(importMapper, mappedInt, this, &MainWindow::slotFileImport);
0369 #else
0370   connect(importMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotFileImport);
0371 #endif
0372 
0373   KActionMenu* importMenu = new KActionMenu(i18n("&Import"), this);
0374   importMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
0375   importMenu->setToolTip(i18n("Import the collection data from other formats"));
0376 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
0377   importMenu->setPopupMode(QToolButton::InstantPopup);
0378 #else
0379   importMenu->setDelayed(false);
0380 #endif
0381   actionCollection()->addAction(QStringLiteral("file_import"), importMenu);
0382 
0383 #define IMPORT_ACTION(TYPE, NAME, TEXT, TIP, ICON) \
0384   action = actionCollection()->addAction(QStringLiteral(NAME), importMapper, mapVoid); \
0385   action->setText(TEXT); \
0386   action->setToolTip(TIP); \
0387   action->setIcon(ICON); \
0388   importMenu->addAction(action); \
0389   importMapper->setMapping(action, TYPE);
0390 
0391   IMPORT_ACTION(Import::TellicoXML, "file_import_tellico", i18n("Import Tellico Data..."),
0392                 i18n("Import another Tellico data file"),
0393                 QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico"))));
0394 
0395   IMPORT_ACTION(Import::CSV, "file_import_csv", i18n("Import CSV Data..."),
0396                 i18n("Import a CSV file"), mimeIcon("text/csv", "text/x-csv"));
0397 
0398   IMPORT_ACTION(Import::MODS, "file_import_mods", i18n("Import MODS Data..."),
0399                 i18n("Import a MODS data file"), mimeIcon("text/xml"));
0400 
0401   IMPORT_ACTION(Import::Alexandria, "file_import_alexandria", i18n("Import Alexandria Data..."),
0402                 i18n("Import data from the Alexandria book collection manager"),
0403                 QIcon::fromTheme(QStringLiteral("alexandria"), QIcon(QLatin1String(":/icons/alexandria"))));
0404 
0405   IMPORT_ACTION(Import::Delicious, "file_import_delicious", i18n("Import Delicious Library Data..."),
0406                 i18n("Import data from Delicious Library"),
0407                 QIcon::fromTheme(QStringLiteral("deliciouslibrary"), QIcon(QLatin1String(":/icons/deliciouslibrary"))));
0408 
0409   IMPORT_ACTION(Import::Collectorz, "file_import_collectorz", i18n("Import Collectorz Data..."),
0410                 i18n("Import data from Collectorz"),
0411                 QIcon::fromTheme(QStringLiteral("collectorz"), QIcon(QLatin1String(":/icons/collectorz"))));
0412 
0413   IMPORT_ACTION(Import::DataCrow, "file_import_datacrow", i18n("Import Data Crow Data..."),
0414                 i18n("Import data from Data Crow"),
0415                 QIcon::fromTheme(QStringLiteral("datacrow"), QIcon(QLatin1String(":/icons/datacrow"))));
0416 
0417   IMPORT_ACTION(Import::Referencer, "file_import_referencer", i18n("Import Referencer Data..."),
0418                 i18n("Import data from Referencer"),
0419                 QIcon::fromTheme(QStringLiteral("referencer"), QIcon(QLatin1String(":/icons/referencer"))));
0420 
0421   IMPORT_ACTION(Import::Bibtex, "file_import_bibtex", i18n("Import Bibtex Data..."),
0422                 i18n("Import a bibtex bibliography file"), mimeIcon("text/x-bibtex"));
0423 #ifndef ENABLE_BTPARSE
0424   action->setEnabled(false);
0425 #endif
0426 
0427   IMPORT_ACTION(Import::Bibtexml, "file_import_bibtexml", i18n("Import Bibtexml Data..."),
0428                 i18n("Import a Bibtexml bibliography file"), mimeIcon("text/xml"));
0429 
0430   IMPORT_ACTION(Import::RIS, "file_import_ris", i18n("Import RIS Data..."),
0431                 i18n("Import an RIS reference file"), QIcon::fromTheme(QStringLiteral(":/icons/cite")));
0432 
0433   IMPORT_ACTION(Import::MARC, "file_import_marc", i18n("Import MARC Data..."),
0434                 i18n("Import MARC data"), QIcon::fromTheme(QStringLiteral(":/icons/cite")));
0435   // disable this import action if the necessary executable is not available
0436   QTimer::singleShot(1000, this, [action]() {
0437     const QString ymd = QStandardPaths::findExecutable(QStringLiteral("yaz-marcdump"));
0438     action->setEnabled(!ymd.isEmpty());
0439   });
0440 
0441   IMPORT_ACTION(Import::Goodreads, "file_import_goodreads", i18n("Import Goodreads Collection..."),
0442                 i18n("Import a collection from Goodreads.com"), QIcon::fromTheme(QStringLiteral(":/icons/goodreads")));
0443 
0444   IMPORT_ACTION(Import::LibraryThing, "file_import_librarything", i18n("Import LibraryThing Collection..."),
0445                 i18n("Import a collection from LibraryThing.com"), QIcon::fromTheme(QStringLiteral(":/icons/librarything")));
0446 
0447   IMPORT_ACTION(Import::PDF, "file_import_pdf", i18n("Import PDF File..."),
0448                 i18n("Import a PDF file"), mimeIcon("application/pdf"));
0449 
0450   IMPORT_ACTION(Import::AudioFile, "file_import_audiofile", i18n("Import Audio File Metadata..."),
0451                 i18n("Import meta-data from audio files"), mimeIcon("audio/mp3", "audio/x-mp3"));
0452 #ifndef HAVE_TAGLIB
0453   action->setEnabled(false);
0454 #endif
0455 
0456   IMPORT_ACTION(Import::FreeDB, "file_import_freedb", i18n("Import Audio CD Data..."),
0457                 i18n("Import audio CD information"), mimeIcon("media/audiocd", "application/x-cda"));
0458 #if !defined (HAVE_OLD_KCDDB) && !defined (HAVE_KCDDB)
0459   action->setEnabled(false);
0460 #endif
0461 
0462   IMPORT_ACTION(Import::Discogs, "file_import_discogs", i18n("Import Discogs Collection..."),
0463                 i18n("Import a collection from Discogs.com"), QIcon::fromTheme(QStringLiteral(":/icons/discogs")));
0464 
0465   IMPORT_ACTION(Import::GCstar, "file_import_gcstar", i18n("Import GCstar Data..."),
0466                 i18n("Import a GCstar data file"),
0467                 QIcon::fromTheme(QStringLiteral("gcstar"), QIcon(QLatin1String(":/icons/gcstar"))));
0468 
0469   IMPORT_ACTION(Import::Griffith, "file_import_griffith", i18n("Import Griffith Data..."),
0470                 i18n("Import a Griffith database"),
0471                 QIcon::fromTheme(QStringLiteral("griffith"), QIcon(QLatin1String(":/icons/griffith"))));
0472 
0473   IMPORT_ACTION(Import::AMC, "file_import_amc", i18n("Import Ant Movie Catalog Data..."),
0474                 i18n("Import an Ant Movie Catalog data file"),
0475                 QIcon::fromTheme(QStringLiteral("amc"), QIcon(QLatin1String(":/icons/amc"))));
0476 
0477   IMPORT_ACTION(Import::BoardGameGeek, "file_import_boardgamegeek", i18n("Import BoardGameGeek Collection..."),
0478                 i18n("Import a collection from BoardGameGeek.com"), QIcon(QLatin1String(":/icons/boardgamegeek")));
0479 
0480   IMPORT_ACTION(Import::VinoXML, "file_import_vinoxml", i18n("Import VinoXML..."),
0481                 i18n("Import VinoXML data"), QIcon(QLatin1String(":/icons/vinoxml")));
0482 
0483   IMPORT_ACTION(Import::FileListing, "file_import_filelisting", i18n("Import File Listing..."),
0484                 i18n("Import information about files in a folder"), mimeIcon("inode/directory"));
0485 
0486   IMPORT_ACTION(Import::XSLT, "file_import_xslt", i18n("Import XSL Transform..."),
0487                 i18n("Import using an XSL Transform"), mimeIcon("application/xslt+xml", "text/x-xslt"));
0488 
0489 #undef IMPORT_ACTION
0490 
0491 /**************** Export Menu ***************************/
0492 
0493   QSignalMapper* exportMapper = new QSignalMapper(this);
0494 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0495   connect(exportMapper, mappedInt, this, &MainWindow::slotFileExport);
0496 #else
0497   connect(exportMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotFileExport);
0498 #endif
0499 
0500   KActionMenu* exportMenu = new KActionMenu(i18n("&Export"), this);
0501   exportMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
0502   exportMenu->setToolTip(i18n("Export the collection data to other formats"));
0503 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
0504   exportMenu->setPopupMode(QToolButton::InstantPopup);
0505 #else
0506   exportMenu->setDelayed(false);
0507 #endif
0508   actionCollection()->addAction(QStringLiteral("file_export"), exportMenu);
0509 
0510 #define EXPORT_ACTION(TYPE, NAME, TEXT, TIP, ICON) \
0511   action = actionCollection()->addAction(QStringLiteral(NAME), exportMapper, mapVoid); \
0512   action->setText(TEXT); \
0513   action->setToolTip(TIP); \
0514   action->setIcon(ICON); \
0515   exportMenu->addAction(action); \
0516   exportMapper->setMapping(action, TYPE);
0517 
0518   EXPORT_ACTION(Export::TellicoXML, "file_export_xml", i18n("Export to XML..."),
0519                 i18n("Export to a Tellico XML file"),
0520                 QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QStringLiteral(":/icons/tellico"))));
0521 
0522   EXPORT_ACTION(Export::TellicoZip, "file_export_zip", i18n("Export to Zip..."),
0523                 i18n("Export to a Tellico Zip file"),
0524                 QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QStringLiteral(":/icons/tellico"))));
0525 
0526   EXPORT_ACTION(Export::HTML, "file_export_html", i18n("Export to HTML..."),
0527                 i18n("Export to an HTML file"), mimeIcon("text/html"));
0528 
0529   EXPORT_ACTION(Export::CSV, "file_export_csv", i18n("Export to CSV..."),
0530                 i18n("Export to a comma-separated values file"), mimeIcon("text/csv", "text/x-csv"));
0531 
0532   EXPORT_ACTION(Export::Alexandria, "file_export_alexandria", i18n("Export to Alexandria..."),
0533                 i18n("Export to an Alexandria library"),
0534                 QIcon::fromTheme(QStringLiteral("alexandria"), QIcon(QStringLiteral(":/icons/alexandria"))));
0535 
0536   EXPORT_ACTION(Export::Bibtex, "file_export_bibtex", i18n("Export to Bibtex..."),
0537                 i18n("Export to a bibtex file"), mimeIcon("text/x-bibtex"));
0538 
0539   EXPORT_ACTION(Export::Bibtexml, "file_export_bibtexml", i18n("Export to Bibtexml..."),
0540                 i18n("Export to a Bibtexml file"), mimeIcon("text/xml"));
0541 
0542   EXPORT_ACTION(Export::ONIX, "file_export_onix", i18n("Export to ONIX..."),
0543                 i18n("Export to an ONIX file"), mimeIcon("text/xml"));
0544 
0545   EXPORT_ACTION(Export::GCstar, "file_export_gcstar", i18n("Export to GCstar..."),
0546                 i18n("Export to a GCstar data file"),
0547                 QIcon::fromTheme(QStringLiteral("gcstar"), QIcon(QStringLiteral(":/icons/gcstar"))));
0548 
0549   EXPORT_ACTION(Export::XSLT, "file_export_xslt", i18n("Export XSL Transform..."),
0550                 i18n("Export using an XSL Transform"), mimeIcon("application/xslt+xml", "text/x-xslt"));
0551 
0552 #undef EXPORT_ACTION
0553 
0554   /*************************************************
0555    * Edit menu
0556    *************************************************/
0557   KStandardAction::undo(Kernel::self()->commandHistory(), SLOT(undo()), actionCollection());
0558   KStandardAction::redo(Kernel::self()->commandHistory(), SLOT(undo()), actionCollection());
0559 
0560   action = KStandardAction::cut(this, SLOT(slotEditCut()), actionCollection());
0561   action->setToolTip(i18n("Cut the selected text and puts it in the clipboard"));
0562   action = KStandardAction::copy(this, SLOT(slotEditCopy()), actionCollection());
0563   action->setToolTip(i18n("Copy the selected text to the clipboard"));
0564   action = KStandardAction::paste(this, SLOT(slotEditPaste()), actionCollection());
0565   action->setToolTip(i18n("Paste the clipboard contents"));
0566   action = KStandardAction::selectAll(this, SLOT(slotEditSelectAll()), actionCollection());
0567   action->setToolTip(i18n("Select all the entries in the collection"));
0568   action = KStandardAction::deselect(this, SLOT(slotEditDeselect()), actionCollection());
0569   action->setToolTip(i18n("Deselect all the entries in the collection"));
0570 
0571   action = actionCollection()->addAction(QStringLiteral("filter_dialog"), this, SLOT(slotShowFilterDialog()));
0572   action->setText(i18n("Advanced &Filter..."));
0573   action->setIconText(i18n("Filter"));
0574   action->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
0575   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_J);
0576   action->setToolTip(i18n("Filter the collection"));
0577 
0578   /*************************************************
0579    * Collection menu
0580    *************************************************/
0581   m_newEntry = actionCollection()->addAction(QStringLiteral("coll_new_entry"),
0582                                              this, SLOT(slotNewEntry()));
0583   m_newEntry->setText(i18n("&New Entry..."));
0584   m_newEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
0585   m_newEntry->setIconText(i18n("New Entry"));
0586   actionCollection()->setDefaultShortcut(m_newEntry, Qt::CTRL + Qt::Key_N);
0587   m_newEntry->setToolTip(i18n("Create a new entry"));
0588 
0589   KActionMenu* addEntryMenu = new KActionMenu(i18n("Add Entry"), this);
0590   addEntryMenu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0591 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
0592   addEntryMenu->setPopupMode(QToolButton::InstantPopup);
0593 #else
0594   addEntryMenu->setDelayed(false);
0595 #endif
0596   actionCollection()->addAction(QStringLiteral("coll_add_entry"), addEntryMenu);
0597 
0598   action = actionCollection()->addAction(QStringLiteral("edit_search_internet"), this, SLOT(slotShowFetchDialog()));
0599   action->setText(i18n("Internet Search..."));
0600   action->setIconText(i18n("Internet Search"));
0601   action->setIcon(QIcon::fromTheme(QStringLiteral("tools-wizard")));
0602   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_I);
0603   action->setToolTip(i18n("Search the internet..."));
0604 
0605   addEntryMenu->addAction(m_newEntry);
0606   addEntryMenu->addAction(actionCollection()->action(QStringLiteral("edit_search_internet")));
0607 
0608   m_editEntry = actionCollection()->addAction(QStringLiteral("coll_edit_entry"),
0609                                               this, SLOT(slotShowEntryEditor()));
0610   m_editEntry->setText(i18n("&Edit Entry..."));
0611   m_editEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
0612   actionCollection()->setDefaultShortcut(m_editEntry, Qt::CTRL + Qt::Key_E);
0613   m_editEntry->setToolTip(i18n("Edit the selected entries"));
0614 
0615   m_copyEntry = actionCollection()->addAction(QStringLiteral("coll_copy_entry"),
0616                                               Controller::self(), SLOT(slotCopySelectedEntries()));
0617   m_copyEntry->setText(i18n("D&uplicate Entry"));
0618   m_copyEntry->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
0619   actionCollection()->setDefaultShortcut(m_copyEntry, Qt::CTRL + Qt::Key_Y);
0620   m_copyEntry->setToolTip(i18n("Copy the selected entries"));
0621 
0622   m_deleteEntry = actionCollection()->addAction(QStringLiteral("coll_delete_entry"),
0623                                                 Controller::self(), SLOT(slotDeleteSelectedEntries()));
0624   m_deleteEntry->setText(i18n("&Delete Entry"));
0625   m_deleteEntry->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
0626   actionCollection()->setDefaultShortcut(m_deleteEntry, Qt::CTRL + Qt::Key_D);
0627   m_deleteEntry->setToolTip(i18n("Delete the selected entries"));
0628 
0629   m_mergeEntry = actionCollection()->addAction(QStringLiteral("coll_merge_entry"),
0630                                                Controller::self(), SLOT(slotMergeSelectedEntries()));
0631   m_mergeEntry->setText(i18n("&Merge Entries"));
0632   m_mergeEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
0633 //  CTRL+G is ambiguous, pick another
0634 //  actionCollection()->setDefaultShortcut(m_mergeEntry, Qt::CTRL + Qt::Key_G);
0635   m_mergeEntry->setToolTip(i18n("Merge the selected entries"));
0636   m_mergeEntry->setEnabled(false); // gets enabled when more than 1 entry is selected
0637 
0638   m_checkOutEntry = actionCollection()->addAction(QStringLiteral("coll_checkout"), Controller::self(), SLOT(slotCheckOut()));
0639   m_checkOutEntry->setText(i18n("Check-&out..."));
0640   m_checkOutEntry->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up-double")));
0641   m_checkOutEntry->setToolTip(i18n("Check-out the selected items"));
0642 
0643   m_checkInEntry = actionCollection()->addAction(QStringLiteral("coll_checkin"), Controller::self(), SLOT(slotCheckIn()));
0644   m_checkInEntry->setText(i18n("Check-&in"));
0645   m_checkInEntry->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down-double")));
0646   m_checkInEntry->setToolTip(i18n("Check-in the selected items"));
0647 
0648   action = actionCollection()->addAction(QStringLiteral("coll_rename_collection"), this, SLOT(slotRenameCollection()));
0649   action->setText(i18n("&Rename Collection..."));
0650   action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
0651   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_R);
0652   action->setToolTip(i18n("Rename the collection"));
0653 
0654   action = actionCollection()->addAction(QStringLiteral("coll_fields"), this, SLOT(slotShowCollectionFieldsDialog()));
0655   action->setText(i18n("Collection &Fields..."));
0656   action->setIconText(i18n("Fields"));
0657   action->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other")));
0658   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_U);
0659   action->setToolTip(i18n("Modify the collection fields"));
0660 
0661   action = actionCollection()->addAction(QStringLiteral("coll_reports"), this, SLOT(slotShowReportDialog()));
0662   action->setText(i18n("&Generate Reports..."));
0663   action->setIconText(i18n("Reports"));
0664   action->setIcon(QIcon::fromTheme(QStringLiteral("text-rdf")));
0665   action->setToolTip(i18n("Generate collection reports"));
0666 
0667   action = actionCollection()->addAction(QStringLiteral("coll_convert_bibliography"), this, SLOT(slotConvertToBibliography()));
0668   action->setText(i18n("Convert to &Bibliography"));
0669   action->setIcon(QIcon(QLatin1String(":/icons/bibtex")));
0670   action->setToolTip(i18n("Convert a book collection to a bibliography"));
0671 
0672   action = actionCollection()->addAction(QStringLiteral("coll_string_macros"), this, SLOT(slotShowStringMacroDialog()));
0673   action->setText(i18n("String &Macros..."));
0674   action->setIcon(QIcon::fromTheme(QStringLiteral("view-list-text")));
0675   action->setToolTip(i18n("Edit the bibtex string macros"));
0676 
0677   action = actionCollection()->addAction(QStringLiteral("coll_key_manager"), this, SLOT(slotShowBibtexKeyDialog()));
0678   action->setText(i18n("Check for Duplicate Keys..."));
0679   action->setIcon(mimeIcon("text/x-bibtex"));
0680   action->setToolTip(i18n("Check for duplicate citation keys"));
0681 
0682   QSignalMapper* citeMapper = new QSignalMapper(this);
0683 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0684   connect(citeMapper, mappedInt, this, &MainWindow::slotCiteEntry);
0685 #else
0686   connect(citeMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotCiteEntry);
0687 #endif
0688 
0689   action = actionCollection()->addAction(QStringLiteral("cite_clipboard"), citeMapper, mapVoid);
0690   action->setText(i18n("Copy Bibtex to Cli&pboard"));
0691   action->setToolTip(i18n("Copy bibtex citations to the clipboard"));
0692   action->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste")));
0693   citeMapper->setMapping(action, Cite::CiteClipboard);
0694 
0695   action = actionCollection()->addAction(QStringLiteral("cite_lyxpipe"), citeMapper, mapVoid);
0696   action->setText(i18n("Cite Entry in &LyX"));
0697   action->setToolTip(i18n("Cite the selected entries in LyX"));
0698   action->setIcon(QIcon::fromTheme(QStringLiteral("lyx"), QIcon(QLatin1String(":/icons/lyx"))));
0699   citeMapper->setMapping(action, Cite::CiteLyxpipe);
0700 
0701   m_updateMapper = new QSignalMapper(this);
0702 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0703   void (QSignalMapper::* mappedString)(const QString&) = &QSignalMapper::mapped;
0704   connect(m_updateMapper, mappedString,
0705           Controller::self(), &Controller::slotUpdateSelectedEntries);
0706 #else
0707   connect(m_updateMapper, &QSignalMapper::mappedString,
0708           Controller::self(), &Controller::slotUpdateSelectedEntries);
0709 #endif
0710 
0711   m_updateEntryMenu = new KActionMenu(i18n("&Update Entry"), this);
0712   m_updateEntryMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
0713   m_updateEntryMenu->setIconText(i18nc("Update Entry", "Update"));
0714 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
0715   m_updateEntryMenu->setPopupMode(QToolButton::InstantPopup);
0716 #else
0717   m_updateEntryMenu->setDelayed(false);
0718 #endif
0719   actionCollection()->addAction(QStringLiteral("coll_update_entry"), m_updateEntryMenu);
0720 
0721   m_updateAll = actionCollection()->addAction(QStringLiteral("update_entry_all"), m_updateMapper, mapVoid);
0722   m_updateAll->setText(i18n("All Sources"));
0723   m_updateAll->setToolTip(i18n("Update entry data from all available sources"));
0724   m_updateMapper->setMapping(m_updateAll, QStringLiteral("_all"));
0725 
0726   /*************************************************
0727    * Settings menu
0728    *************************************************/
0729   setStandardToolBarMenuEnabled(true);
0730   createStandardStatusBarAction();
0731 
0732   m_lockLayout = new KDualAction(this);
0733   connect(m_lockLayout, &KDualAction::activeChanged, this, &MainWindow::slotToggleLayoutLock);
0734   m_lockLayout->setActiveText(i18n("Unlock Layout"));
0735   m_lockLayout->setActiveToolTip(i18n("Unlock the window's layout"));
0736   m_lockLayout->setActiveIcon(QIcon::fromTheme(QStringLiteral("object-unlocked")));
0737   m_lockLayout->setInactiveText(i18n("Lock Layout"));
0738   m_lockLayout->setInactiveToolTip(i18n("Lock the window's layout"));
0739   m_lockLayout->setInactiveIcon(QIcon::fromTheme(QStringLiteral("object-locked")));
0740   actionCollection()->addAction(QStringLiteral("lock_layout"), m_lockLayout);
0741 
0742   action = actionCollection()->addAction(QStringLiteral("reset_layout"), this, SLOT(slotResetLayout()));
0743   action->setText(i18n("Reset Layout"));
0744   action->setToolTip(i18n("Reset the window's layout"));
0745   action->setIcon(QIcon::fromTheme(QStringLiteral("resetview")));
0746 
0747   m_toggleEntryEditor = new KToggleAction(i18n("Entry &Editor"), this);
0748   connect(m_toggleEntryEditor, &QAction::triggered, this, &MainWindow::slotToggleEntryEditor);
0749   m_toggleEntryEditor->setToolTip(i18n("Enable/disable the editor"));
0750   actionCollection()->addAction(QStringLiteral("toggle_edit_widget"), m_toggleEntryEditor);
0751 
0752   KStandardAction::preferences(this, SLOT(slotShowConfigDialog()), actionCollection());
0753 
0754   /*************************************************
0755    * Help menu
0756    *************************************************/
0757   action = actionCollection()->addAction(QStringLiteral("show_log"), this, SLOT(showLog()));
0758   action->setText(i18n("Show Log"));
0759   action->setIcon(QIcon::fromTheme(QStringLiteral("view-history")));
0760   if(Logger::self()->logFile().isEmpty()) {
0761     action->setEnabled(false);
0762   }
0763 
0764 
0765   /*************************************************
0766    * Short cuts
0767    *************************************************/
0768   KStandardAction::fullScreen(this, SLOT(slotToggleFullScreen()), this, actionCollection());
0769   KStandardAction::showMenubar(this, SLOT(slotToggleMenuBarVisibility()), actionCollection());
0770 
0771   /*************************************************
0772    * Collection Toolbar
0773    *************************************************/
0774   action = actionCollection()->addAction(QStringLiteral("change_entry_grouping_accel"), this, SLOT(slotGroupLabelActivated()));
0775   action->setText(i18n("Change Grouping"));
0776   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_G);
0777 
0778   m_entryGrouping = new KSelectAction(i18n("&Group Selection"), this);
0779   m_entryGrouping->setToolTip(i18n("Change the grouping of the collection"));
0780 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,78,0)
0781   void (KSelectAction::* triggeredInt)(int) = &KSelectAction::indexTriggered;
0782 #else
0783   void (KSelectAction::* triggeredInt)(int) = &KSelectAction::triggered;
0784 #endif
0785   connect(m_entryGrouping, triggeredInt, this, &MainWindow::slotChangeGrouping);
0786   actionCollection()->addAction(QStringLiteral("change_entry_grouping"), m_entryGrouping);
0787 
0788   action = actionCollection()->addAction(QStringLiteral("quick_filter_accel"), this, SLOT(slotFilterLabelActivated()));
0789   action->setText(i18n("Filter"));
0790   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F);
0791 
0792   m_quickFilter = new GUI::LineEdit(this);
0793   m_quickFilter->setPlaceholderText(i18n("Filter here...")); // same text as kdepim and amarok
0794   m_quickFilter->setClearButtonEnabled(true);
0795   // same as Dolphin text edit
0796   m_quickFilter->setMinimumWidth(150);
0797   m_quickFilter->setMaximumWidth(300);
0798   // want to update every time the filter text changes
0799   connect(m_quickFilter, &QLineEdit::textChanged,
0800           this, &MainWindow::slotQueueFilter);
0801   connect(m_quickFilter, &KLineEdit::clearButtonClicked,
0802           this, &MainWindow::slotClearFilter);
0803   m_quickFilter->installEventFilter(this); // intercept keyEvents
0804 
0805   QWidgetAction* widgetAction = new QWidgetAction(this);
0806   widgetAction->setDefaultWidget(m_quickFilter);
0807   widgetAction->setText(i18n("Filter"));
0808   widgetAction->setToolTip(i18n("Filter the collection"));
0809   widgetAction->setProperty("isShortcutConfigurable", false);
0810   actionCollection()->addAction(QStringLiteral("quick_filter"), widgetAction);
0811 
0812   // final GUI setup is in initView()
0813 }
0814 
0815 #undef mimeIcon
0816 
0817 void MainWindow::initDocument() {
0818   MARK;
0819   Data::Document* doc = Data::Document::self();
0820   Kernel::self()->resetHistory();
0821 
0822   KConfigGroup config(KSharedConfig::openConfig(), "General Options");
0823   doc->setLoadAllImages(config.readEntry("Load All Images", false));
0824 
0825   // allow status messages from the document
0826   connect(doc, &Data::Document::signalStatusMsg,
0827           this, &MainWindow::slotStatusMsg);
0828 
0829   // do stuff that changes when the doc is modified
0830   connect(doc, &Data::Document::signalModified,
0831           this, &MainWindow::slotEnableModifiedActions);
0832 
0833   connect(doc, &Data::Document::signalCollectionAdded,
0834           Controller::self(), &Controller::slotCollectionAdded);
0835   connect(doc, &Data::Document::signalCollectionDeleted,
0836           Controller::self(), &Controller::slotCollectionDeleted);
0837   connect(doc, &Data::Document::signalCollectionModified,
0838           Controller::self(), &Controller::slotCollectionModified);
0839 
0840   connect(Kernel::self()->commandHistory(), &QUndoStack::cleanChanged,
0841           doc, &Data::Document::slotSetClean);
0842 }
0843 
0844 void MainWindow::initView() {
0845   MARK;
0846   // initialize the image factory before the entry models are created
0847   ImageFactory::init();
0848 
0849   m_entryView = new EntryView(this);
0850   connect(m_entryView, &EntryView::signalTellicoAction,
0851           this, &MainWindow::slotURLAction);
0852 
0853   // trick to make sure the group views always extend along the entire left or right side
0854   // using QMainWindow::setCorner does not seem to work
0855   // https://wiki.qt.io/Technical_FAQ#Is_it_possible_for_either_the_left_or_right_dock_areas_to_have_full_height_of_their_side_rather_than_having_the_bottom_take_the_full_width.3F
0856   m_dummyWindow = new QMainWindow(this);
0857 #ifdef USE_KHTML
0858   m_entryView->view()->setWhatsThis(i18n("<qt>The <i>Entry View</i> shows a formatted view of the entry's contents.</qt>"));
0859   m_dummyWindow->setCentralWidget(m_entryView->view());
0860 #else
0861   m_entryView->setWhatsThis(i18n("<qt>The <i>Entry View</i> shows a formatted view of the entry's contents.</qt>"));
0862   m_dummyWindow->setCentralWidget(m_entryView);
0863 #endif
0864   m_dummyWindow->setWindowFlags(Qt::Widget);
0865   setCentralWidget(m_dummyWindow);
0866 
0867   m_collectionViewDock = new GUI::DockWidget(i18n("Collection View"), m_dummyWindow);
0868   m_collectionViewDock->setObjectName(QStringLiteral("collection_dock"));
0869 
0870   m_viewStack = new ViewStack(this);
0871 
0872   m_detailedView = m_viewStack->listView();
0873   Controller::self()->addObserver(m_detailedView);
0874   m_detailedView->setWhatsThis(i18n("<qt>The <i>Column View</i> shows the value of multiple fields "
0875                                        "for each entry.</qt>"));
0876   connect(Data::Document::self(), &Data::Document::signalCollectionImagesLoaded,
0877           m_detailedView, &DetailedListView::slotRefreshImages);
0878 
0879   m_iconView = m_viewStack->iconView();
0880   EntryIconModel* iconModel = new EntryIconModel(m_iconView);
0881   iconModel->setSourceModel(m_detailedView->model());
0882   m_iconView->setModel(iconModel);
0883   Controller::self()->addObserver(m_iconView);
0884   m_iconView->setWhatsThis(i18n("<qt>The <i>Icon View</i> shows each entry in the collection or group using "
0885                                 "an icon, which may be an image in the entry.</qt>"));
0886 
0887   m_collectionViewDock->setWidget(m_viewStack);
0888   m_dummyWindow->addDockWidget(Qt::TopDockWidgetArea, m_collectionViewDock);
0889   actionCollection()->addAction(QStringLiteral("toggle_column_widget"), m_collectionViewDock->toggleViewAction());
0890 
0891   m_groupViewDock = new GUI::DockWidget(i18n("Group View"), this);
0892   m_groupViewDock->setObjectName(QStringLiteral("group_dock"));
0893   m_groupViewDock->setAllowedAreas(Qt::DockWidgetAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea));
0894 
0895   m_viewTabs = new GUI::TabWidget(this);
0896   m_viewTabs->setTabBarHidden(true);
0897   m_viewTabs->setDocumentMode(true);
0898   m_groupView = new GroupView(m_viewTabs);
0899   Controller::self()->addObserver(m_groupView);
0900   m_viewTabs->addTab(m_groupView, QIcon::fromTheme(QStringLiteral("folder")), i18n("Groups"));
0901   m_groupView->setWhatsThis(i18n("<qt>The <i>Group View</i> sorts the entries into groupings "
0902                                     "based on a selected field.</qt>"));
0903   m_groupViewDock->setWidget(m_viewTabs);
0904   addDockWidget(Qt::LeftDockWidgetArea, m_groupViewDock);
0905   actionCollection()->addAction(QStringLiteral("toggle_group_widget"), m_groupViewDock->toggleViewAction());
0906 
0907   EntrySelectionModel* proxySelect = new EntrySelectionModel(m_iconView->model(),
0908                                                              m_detailedView->selectionModel(),
0909                                                              this);
0910   m_iconView->setSelectionModel(proxySelect);
0911 
0912   // setting up GUI now rather than in initActions
0913   // initial parameter is default window size
0914   setupGUI(QSize(1280,800), Keys | ToolBar);
0915   createGUI();
0916 }
0917 
0918 void MainWindow::initConnections() {
0919   // have to toggle the menu item if the dialog gets closed
0920   connect(m_editDialog, &QDialog::finished,
0921           this, &MainWindow::slotEditDialogFinished);
0922 
0923   EntrySelectionModel* proxySelect = static_cast<EntrySelectionModel*>(m_iconView->selectionModel());
0924   connect(proxySelect, &EntrySelectionModel::entriesSelected,
0925           Controller::self(), &Controller::slotUpdateSelection);
0926   connect(proxySelect, &EntrySelectionModel::entriesSelected,
0927           m_editDialog, &EntryEditDialog::setContents);
0928   connect(proxySelect, &EntrySelectionModel::entriesSelected,
0929           m_entryView, &EntryView::showEntries);
0930 
0931   // let the group view call filters, too
0932   connect(m_groupView, &GroupView::signalUpdateFilter,
0933           this, &MainWindow::slotUpdateFilter);
0934   // use the EntrySelectionModel as a proxy so when entries get selected in the group view
0935   // the edit dialog and entry view are updated
0936   proxySelect->addSelectionProxy(m_groupView->selectionModel());
0937 }
0938 
0939 void MainWindow::initFileOpen(bool nofile_) {
0940   MARK;
0941   slotInit();
0942   // check to see if most recent file should be opened
0943   bool happyStart = false;
0944   if(!nofile_ && Config::reopenLastFile()) {
0945     // Config::lastOpenFile() is the full URL, protocol included
0946     QUrl lastFile(Config::lastOpenFile()); // empty string is actually ok, it gets handled
0947     if(!lastFile.isEmpty() && lastFile.isValid()) {
0948       myLog() << "Opening previous file:" << lastFile.toDisplayString(QUrl::PreferLocalFile);
0949       slotFileOpen(lastFile);
0950       happyStart = true;
0951     }
0952   }
0953   if(!happyStart) {
0954     myLog() << "Creating default book collection";
0955     // the document is created with an initial book collection, continue with that
0956     Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
0957 
0958     m_fileSave->setEnabled(false);
0959     slotEnableOpenedActions();
0960     slotEnableModifiedActions(false);
0961 
0962     slotEntryCount();
0963     // tell the entry views and models that there are no images to load
0964     m_detailedView->slotRefreshImages();
0965   }
0966 
0967   // show welcome text, even when opening an existing collection
0968   const int type = Kernel::self()->collectionType();
0969   QString welcomeFile = DataFileRegistry::self()->locate(QStringLiteral("welcome.html"));
0970   QString text = FileHandler::readTextFile(QUrl::fromLocalFile(welcomeFile));
0971   text.replace(QLatin1String("$FGCOLOR$"), Config::templateTextColor(type).name());
0972   text.replace(QLatin1String("$BGCOLOR$"), Config::templateBaseColor(type).name());
0973   text.replace(QLatin1String("$COLOR1$"),  Config::templateHighlightedTextColor(type).name());
0974   text.replace(QLatin1String("$COLOR2$"),  Config::templateHighlightedBaseColor(type).name());
0975   text.replace(QLatin1String("$LINKCOLOR$"), Config::templateLinkColor(type).name());
0976   text.replace(QLatin1String("$IMGDIR$"),  QUrl::fromLocalFile(ImageFactory::imageDir()).url());
0977   text.replace(QLatin1String("$BANNER$"),
0978                 i18n("Welcome to the Tellico Collection Manager"));
0979   text.replace(QLatin1String("$WELCOMETEXT$"),
0980                 i18n("<h3>Tellico is a tool for managing collections of books, "
0981                     "videos, music, and whatever else you want to catalog.</h3>"
0982                     "<h3>New entries can be added to your collection by "
0983                     "<a href=\"tc:///coll_new_entry\">entering data manually</a> or by "
0984                     "<a href=\"tc:///edit_search_internet\">downloading data</a> from "
0985                     "various Internet sources.</h3>"));
0986   m_entryView->showText(text);
0987 
0988   m_initialized = true;
0989 }
0990 
0991 // These are general options.
0992 // The options that can be changed in the "Configuration..." dialog
0993 // are taken care of by the ConfigDialog object.
0994 void MainWindow::saveOptions() {
0995   KConfigGroup config(KSharedConfig::openConfig(), "Main Window Options");
0996   saveMainWindowSettings(config);
0997   config.writeEntry(QStringLiteral("Central Dock State"), m_dummyWindow->saveState());
0998 
0999   Config::setShowEditWidget(m_toggleEntryEditor->isChecked());
1000   // check any single dock widget, they all get locked together
1001   Config::setLockLayout(m_groupViewDock->isLocked());
1002 
1003   KConfigGroup filesConfig(KSharedConfig::openConfig(), "Recent Files");
1004   m_fileOpenRecent->saveEntries(filesConfig);
1005   if(!isNewDocument()) {
1006     Config::setLastOpenFile(Data::Document::self()->URL().url());
1007   }
1008 
1009   Config::setViewWidget(m_viewStack->currentWidget());
1010 
1011   // historical reasons
1012   // sorting by count was faked by sorting by phantom second column
1013   const int sortColumn = m_groupView->sortRole() == RowCountRole ? 1 : 0;
1014   Config::setGroupViewSortColumn(sortColumn); // ok to use SortColumn key, save semantics
1015   Config::setGroupViewSortAscending(m_groupView->sortOrder() == Qt::AscendingOrder);
1016 
1017   if(m_loanView) {
1018     const int sortColumn = m_loanView->sortRole() == RowCountRole ? 1 : 0;
1019     Config::setLoanViewSortAscending(sortColumn); // ok to use SortColumn key, save semantics
1020     Config::setLoanViewSortAscending(m_loanView->sortOrder() == Qt::AscendingOrder);
1021   }
1022 
1023   if(m_filterView) {
1024     const int sortColumn = m_filterView->sortRole() == RowCountRole ? 1 : 0;
1025     Config::setFilterViewSortAscending(sortColumn); // ok to use SortColumn key, save semantics
1026     Config::setFilterViewSortAscending(m_filterView->sortOrder() == Qt::AscendingOrder);
1027   }
1028 
1029   // this is used in the EntryEditDialog constructor, too
1030   KConfigGroup editDialogConfig(KSharedConfig::openConfig(), "Edit Dialog Options");
1031   KWindowConfig::saveWindowSize(m_editDialog->windowHandle(), editDialogConfig);
1032 
1033   saveCollectionOptions(Data::Document::self()->collection());
1034   Config::self()->save();
1035 }
1036 
1037 void MainWindow::readCollectionOptions(Tellico::Data::CollPtr coll_) {
1038   if(!coll_) {
1039     myDebug() << "Bad, no collection in MainWindow::readCollectionOptions()";
1040     return;
1041   }
1042   const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_));
1043   KConfigGroup group(KSharedConfig::openConfig(), configGroup);
1044 
1045   QString defaultGroup = coll_->defaultGroupField();
1046   QString entryGroup, groupSortField;
1047   if(coll_->type() != Data::Collection::Base) {
1048     entryGroup = group.readEntry("Group By", defaultGroup);
1049     groupSortField = group.readEntry("GroupEntrySortField", QString());
1050   } else {
1051     QUrl url = Kernel::self()->URL();
1052     for(int i = 0; i < Config::maxCustomURLSettings(); ++i) {
1053       QUrl u(group.readEntry(QStringLiteral("URL_%1").arg(i)));
1054       if(url == u) {
1055         entryGroup = group.readEntry(QStringLiteral("Group By_%1").arg(i), defaultGroup);
1056         groupSortField = group.readEntry(QStringLiteral("GroupEntrySortField_%1").arg(i), QString());
1057         break;
1058       }
1059     }
1060     // fall back to old setting
1061     if(entryGroup.isEmpty()) {
1062       entryGroup = group.readEntry("Group By", defaultGroup);
1063     }
1064   }
1065   if(entryGroup.isEmpty() ||
1066      (!coll_->entryGroups().contains(entryGroup) && entryGroup != Data::Collection::s_peopleGroupName)) {
1067     entryGroup = defaultGroup;
1068   }
1069   m_groupView->setGroupField(entryGroup);
1070 
1071   if(!groupSortField.isEmpty()) {
1072     m_groupView->setEntrySortField(groupSortField);
1073   }
1074 
1075   QString entryXSLTFile;
1076   if(coll_->type() == Data::Collection::Base &&
1077      Data::Document::self()->URL().fileName() != i18n(Tellico::untitledFilename)) {
1078     // use a nested config group for template specific to custom collections
1079     // using the filename alone as a keyEvents
1080     KConfigGroup subGroup(&group, Data::Document::self()->URL().fileName());
1081     entryXSLTFile = subGroup.readEntry(QStringLiteral("Template Name"));
1082   }
1083   if(entryXSLTFile.isEmpty()) {
1084     // lookup by collection type
1085     entryXSLTFile = Config::templateName(coll_->type());
1086   }
1087   if(entryXSLTFile.isEmpty()) {
1088     entryXSLTFile = QStringLiteral("Fancy"); // should never happen, but just in case
1089   }
1090   m_entryView->setXSLTFile(entryXSLTFile + QLatin1String(".xsl"));
1091 
1092   // make sure the right combo element is selected
1093   slotUpdateCollectionToolBar(coll_);
1094 }
1095 
1096 void MainWindow::saveCollectionOptions(Tellico::Data::CollPtr coll_) {
1097   // don't save initial collection options, or empty collections
1098   if(!coll_ || coll_->entryCount() == 0 || isNewDocument()) {
1099     return;
1100   }
1101 
1102   int configIndex = -1;
1103   QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_));
1104   KConfigGroup config(KSharedConfig::openConfig(), configGroup);
1105   QString groupName;
1106   const QString groupEntrySort = m_groupView->entrySortField();
1107   if(m_entryGrouping->currentItem() > -1 &&
1108      static_cast<int>(coll_->entryGroups().count()) > m_entryGrouping->currentItem()) {
1109     if(m_entryGrouping->currentText() == (QLatin1Char('<') + i18n("People") + QLatin1Char('>'))) {
1110       groupName = Data::Collection::s_peopleGroupName;
1111     } else {
1112       groupName = Data::Document::self()->collection()->fieldNameByTitle(m_entryGrouping->currentText());
1113     }
1114     if(coll_->type() != Data::Collection::Base) {
1115       config.writeEntry("Group By", groupName);
1116       if(!groupEntrySort.isEmpty()) {
1117         config.writeEntry("GroupEntrySortField", groupEntrySort);
1118       }
1119     }
1120   }
1121 
1122   if(coll_->type() == Data::Collection::Base) {
1123     // all of this is to have custom settings on a per file basis
1124     QUrl url = Kernel::self()->URL();
1125     QList<QUrl> urls = QList<QUrl>() << url;
1126     QStringList groupBys = QStringList() << groupName;
1127     QStringList groupSorts = QStringList() << groupEntrySort;
1128     for(int i = 0; i < Config::maxCustomURLSettings(); ++i) {
1129       QUrl u = config.readEntry(QStringLiteral("URL_%1").arg(i), QUrl());
1130       if(!u.isEmpty() && url != u) {
1131         urls.append(u);
1132         QString g = config.readEntry(QStringLiteral("Group By_%1").arg(i), QString());
1133         groupBys.append(g);
1134         QString gs = config.readEntry(QStringLiteral("GroupEntrySortField_%1").arg(i), QString());
1135         groupSorts.append(gs);
1136       } else if(!u.isEmpty()) {
1137         configIndex = i;
1138       }
1139     }
1140     int limit = qMin(urls.count(), Config::maxCustomURLSettings());
1141     for(int i = 0; i < limit; ++i) {
1142       config.writeEntry(QStringLiteral("URL_%1").arg(i), urls[i].url());
1143       config.writeEntry(QStringLiteral("Group By_%1").arg(i), groupBys[i]);
1144       config.writeEntry(QStringLiteral("GroupEntrySortField_%1").arg(i), groupSorts[i]);
1145     }
1146   }
1147   m_detailedView->saveConfig(coll_, configIndex);
1148 }
1149 
1150 void MainWindow::readOptions() {
1151   KConfigGroup mainWindowConfig(KSharedConfig::openConfig(), "Main Window Options");
1152   applyMainWindowSettings(mainWindowConfig);
1153   m_dummyWindow->restoreState(mainWindowConfig.readEntry(QStringLiteral("Central Dock State"), QByteArray()));
1154 
1155   m_viewStack->setCurrentWidget(Config::viewWidget());
1156   m_iconView->setMaxAllowedIconWidth(Config::maxIconSize());
1157 
1158   connect(toolBar(QStringLiteral("collectionToolBar")), &QToolBar::iconSizeChanged, this, &MainWindow::slotUpdateToolbarIcons);
1159 
1160   // initialize the recent file list
1161   KConfigGroup filesConfig(KSharedConfig::openConfig(), "Recent Files");
1162   m_fileOpenRecent->loadEntries(filesConfig);
1163 
1164   // sort by count if column = 1
1165   int sortRole = Config::groupViewSortColumn() == 0 ? static_cast<int>(Qt::DisplayRole) : static_cast<int>(RowCountRole);
1166   Qt::SortOrder sortOrder = Config::groupViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder;
1167   m_groupView->setSorting(sortOrder, sortRole);
1168 
1169   BibtexHandler::s_quoteStyle = Config::useBraces() ? BibtexHandler::BRACES : BibtexHandler::QUOTES;
1170 
1171   // Don't read any options for the edit dialog here, since it's not yet initialized.
1172   // Put them in init()
1173 }
1174 
1175 bool MainWindow::querySaveModified() {
1176   bool completed = true;
1177 
1178   if(Data::Document::self()->isModified()) {
1179     QString str = i18n("The current file has been modified.\n"
1180                        "Do you want to save it?");
1181 #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
1182     auto want_save = KMessageBox::warningYesNoCancel(this, str, i18n("Unsaved Changes"),
1183                                                      KStandardGuiItem::save(), KStandardGuiItem::discard());
1184     switch(want_save) {
1185       case KMessageBox::Yes:
1186         completed = fileSave();
1187         break;
1188 
1189       case KMessageBox::No:
1190         Data::Document::self()->setModified(false);
1191         completed = true;
1192         break;
1193 
1194       case KMessageBox::Cancel:
1195       default:
1196         completed = false;
1197         break;
1198     }
1199 #else
1200     auto want_save = KMessageBox::warningTwoActionsCancel(this, str, i18n("Unsaved Changes"),
1201                                                           KStandardGuiItem::save(), KStandardGuiItem::discard());
1202     switch(want_save) {
1203       case KMessageBox::ButtonCode::PrimaryAction:
1204         completed = fileSave();
1205         break;
1206 
1207       case KMessageBox::ButtonCode::SecondaryAction:
1208         Data::Document::self()->setModified(false);
1209         completed = true;
1210         break;
1211 
1212       case KMessageBox::ButtonCode::Cancel:
1213       default:
1214         completed = false;
1215         break;
1216     }
1217 #endif
1218   }
1219 
1220   return completed;
1221 }
1222 
1223 bool MainWindow::queryClose() {
1224   // in case we're still loading the images, cancel that
1225   Data::Document::self()->cancelImageWriting();
1226   const bool willClose = m_editDialog->queryModified() && querySaveModified();
1227   if(willClose) {
1228     ImageFactory::clean(true);
1229     saveOptions();
1230   }
1231   return willClose;
1232 }
1233 
1234 void MainWindow::slotFileNew(int type_) {
1235   slotStatusMsg(i18n("Creating new collection..."));
1236 
1237   // close the fields dialog
1238   slotHideCollectionFieldsDialog();
1239 
1240   if(m_editDialog->queryModified() && querySaveModified()) {
1241     // remove filter and loan tabs, they'll get re-added if needed
1242     if(m_filterView) {
1243       m_viewTabs->removeTab(m_viewTabs->indexOf(m_filterView));
1244       Controller::self()->removeObserver(m_filterView);
1245       delete m_filterView;
1246       m_filterView = nullptr;
1247     }
1248     if(m_loanView) {
1249       m_viewTabs->removeTab(m_viewTabs->indexOf(m_loanView));
1250       Controller::self()->removeObserver(m_loanView);
1251       delete m_loanView;
1252       m_loanView = nullptr;
1253     }
1254     m_viewTabs->setTabBarHidden(true);
1255     Data::Document::self()->newDocument(type_);
1256     myLog() << "Creating new collection, type" << CollectionFactory::typeName(type_);
1257     Kernel::self()->resetHistory();
1258     m_fileOpenRecent->setCurrentItem(-1);
1259     slotEnableOpenedActions();
1260     slotEnableModifiedActions(false);
1261     m_newDocument = true;
1262     ImageFactory::clean(false);
1263   }
1264 
1265   StatusBar::self()->clearStatus();
1266 }
1267 
1268 void MainWindow::slotFileNewByTemplate(const QString& collectionTemplate_) {
1269   slotStatusMsg(i18n("Creating new collection..."));
1270 
1271   // close the fields dialog
1272   slotHideCollectionFieldsDialog();
1273 
1274   if(m_editDialog->queryModified() && querySaveModified()) {
1275     openURL(QUrl::fromLocalFile(collectionTemplate_));
1276     myLog() << "Creating new collection from template:" << collectionTemplate_;
1277     Data::Document::self()->setURL(QUrl::fromLocalFile(i18n(Tellico::untitledFilename)));
1278     Kernel::self()->resetHistory();
1279     m_fileOpenRecent->setCurrentItem(-1);
1280     slotEnableOpenedActions();
1281     slotEnableModifiedActions(false);
1282     m_newDocument = true;
1283     ImageFactory::clean(false);
1284   }
1285 
1286   StatusBar::self()->clearStatus();
1287 }
1288 
1289 void MainWindow::slotFileOpen() {
1290   slotStatusMsg(i18n("Opening file..."));
1291 
1292   if(m_editDialog->queryModified() && querySaveModified()) {
1293     QString filter = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)");
1294     filter += QLatin1String(";;");
1295     filter += i18n("XML Files") + QLatin1String(" (*.xml)");
1296     filter += QLatin1String(";;");
1297     filter += i18n("All Files") + QLatin1String(" (*)");
1298     // keyword 'open'
1299     QString fileClass;
1300     const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///open")), fileClass);
1301     QUrl url = QFileDialog::getOpenFileUrl(this, i18n("Open File"), startUrl, filter);
1302     if(!url.isEmpty() && url.isValid()) {
1303       myLog() << "Opening collection file:" << url.toDisplayString(QUrl::PreferLocalFile);
1304       slotFileOpen(url);
1305       if(url.isLocalFile()) {
1306         KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1307       }
1308     }
1309   }
1310   StatusBar::self()->clearStatus();
1311 }
1312 
1313 void MainWindow::slotFileOpen(const QUrl& url_) {
1314   slotStatusMsg(i18n("Opening file..."));
1315 
1316   // close the fields dialog
1317   slotHideCollectionFieldsDialog();
1318 
1319   // there seems to be a race condition at start between slotInit() and initFileOpen()
1320   // which means the edit dialog might not have been created yet
1321   if((!m_editDialog || m_editDialog->queryModified()) && querySaveModified()) {
1322     if(openURL(url_)) {
1323       m_fileOpenRecent->addUrl(url_);
1324       m_fileOpenRecent->setCurrentItem(-1);
1325     }
1326   }
1327 
1328   StatusBar::self()->clearStatus();
1329 }
1330 
1331 void MainWindow::slotFileOpenRecent(const QUrl& url_) {
1332   slotStatusMsg(i18n("Opening file..."));
1333 
1334   // close the fields dialog
1335   slotHideCollectionFieldsDialog();
1336 
1337   if(m_editDialog->queryModified() && querySaveModified()) {
1338     if(!openURL(url_)) {
1339       m_fileOpenRecent->removeUrl(url_);
1340       m_fileOpenRecent->setCurrentItem(-1);
1341     }
1342   } else {
1343     // the QAction shouldn't be checked now
1344     m_fileOpenRecent->setCurrentItem(-1);
1345   }
1346 
1347   StatusBar::self()->clearStatus();
1348 }
1349 
1350 void MainWindow::openFile(const QString& file_) {
1351   QUrl url(file_);
1352   if(!url.isEmpty() && url.isValid()) {
1353     slotFileOpen(url);
1354   }
1355 }
1356 
1357 bool MainWindow::openURL(const QUrl& url_) {
1358   MARK;
1359   // try to open document
1360   GUI::CursorSaver cs(Qt::WaitCursor);
1361 
1362   bool success = Data::Document::self()->openDocument(url_);
1363 
1364   if(success) {
1365     Kernel::self()->resetHistory();
1366     m_quickFilter->clear();
1367     slotEnableOpenedActions();
1368     m_newDocument = false;
1369     slotEnableModifiedActions(Data::Document::self()->isModified()); // doc might add some stuff
1370   } else if(!m_initialized) {
1371     // special case on startup when openURL() is called with a command line argument
1372     // and that URL can't be opened. The window still needs to be initialized
1373     // the doc object is created with an initial book collection, continue with that
1374     Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
1375 
1376     m_fileSave->setEnabled(false);
1377     slotEnableOpenedActions();
1378     slotEnableModifiedActions(false);
1379 
1380     slotEntryCount();
1381   }
1382   // slotFileOpen(URL) gets called when opening files on the command line
1383   // so go ahead and make sure m_initialized is set.
1384   m_initialized = true;
1385 
1386   // remove filter and loan tabs, they'll get re-added if needed
1387   if(m_filterView && m_filterView->isEmpty()) {
1388     m_viewTabs->removeTab(m_viewTabs->indexOf(m_filterView));
1389     Controller::self()->removeObserver(m_filterView);
1390     delete m_filterView;
1391     m_filterView = nullptr;
1392   }
1393   if(m_loanView && m_loanView->isEmpty()) {
1394     m_viewTabs->removeTab(m_viewTabs->indexOf(m_loanView));
1395     Controller::self()->removeObserver(m_loanView);
1396     delete m_loanView;
1397     m_loanView = nullptr;
1398   }
1399   Controller::self()->hideTabs(); // does conditional check
1400 
1401   return success;
1402 }
1403 
1404 void MainWindow::slotFileSave() {
1405   fileSave();
1406 }
1407 
1408 bool MainWindow::fileSave() {
1409   if(!m_editDialog->queryModified()) {
1410     return false;
1411   }
1412   slotStatusMsg(i18n("Saving file..."));
1413 
1414   bool ret = true;
1415   if(isNewDocument()) {
1416     ret = fileSaveAs();
1417   } else {
1418     // special check: if there are more than 200 images AND the "Write Images In File" config key
1419     // is set, then warn user that performance may suffer, and write result
1420     if(Config::imageLocation() == Config::ImagesInFile &&
1421        Config::askWriteImagesInFile() &&
1422        Data::Document::self()->imageCount() > MAX_IMAGES_WARN_PERFORMANCE) {
1423       QString msg = i18n("<qt><p>You are saving a file with many images, which causes Tellico to "
1424                          "slow down significantly. Do you want to save the images separately in "
1425                          "Tellico's data directory to improve performance?</p><p>Your choice can "
1426                          "always be changed in the configuration dialog.</p></qt>");
1427 
1428       KGuiItem yes(i18n("Save Images Separately"));
1429       KGuiItem no(i18n("Save Images in File"));
1430 
1431 #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
1432       auto res = KMessageBox::warningYesNo(this, msg, QString() /* caption */, yes, no);
1433       if(res == KMessageBox::No) {
1434 #else
1435       auto res = KMessageBox::warningTwoActions(this, msg, QString() /* caption */, yes, no);
1436       if(res == KMessageBox::ButtonCode::SecondaryAction) {
1437 #endif
1438         Config::setImageLocation(Config::ImagesInAppDir);
1439       }
1440       Config::setAskWriteImagesInFile(false);
1441     }
1442 
1443     GUI::CursorSaver cs(Qt::WaitCursor);
1444     if(Data::Document::self()->saveDocument(Data::Document::self()->URL())) {
1445       Kernel::self()->resetHistory();
1446       m_newDocument = false;
1447       updateCaption(false);
1448       m_fileSave->setEnabled(false);
1449       // TODO: call a method of the model instead of the view here
1450       m_detailedView->resetEntryStatus();
1451     } else {
1452       ret = false;
1453     }
1454   }
1455 
1456   StatusBar::self()->clearStatus();
1457   return ret;
1458 }
1459 
1460 void MainWindow::slotFileSaveAs() {
1461   fileSaveAs();
1462 }
1463 
1464 bool MainWindow::fileSaveAs() {
1465   if(!m_editDialog->queryModified()) {
1466     return false;
1467   }
1468 
1469   slotStatusMsg(i18n("Saving file with a new filename..."));
1470 
1471   QString filter = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)");
1472   filter += QLatin1String(";;");
1473   filter += i18n("All Files") + QLatin1String(" (*)");
1474 
1475   // keyword 'open'
1476   QString fileClass;
1477   const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///open")), fileClass);
1478   const QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Save As"), startUrl, filter);
1479 
1480   if(url.isEmpty()) {
1481     StatusBar::self()->clearStatus();
1482     return false;
1483   }
1484   if(url.isLocalFile()) {
1485     KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1486   }
1487 
1488   bool ret = true;
1489   if(url.isValid()) {
1490     GUI::CursorSaver cs(Qt::WaitCursor);
1491     m_savingImageLocationChange = true;
1492     // Overwriting an existing file was already confirmed in QFileDialog::getSaveFileUrl()
1493     if(Data::Document::self()->saveDocument(url, true /* force */)) {
1494       Kernel::self()->resetHistory();
1495       KRecentDocument::add(url);
1496       m_fileOpenRecent->addUrl(url);
1497       updateCaption(false);
1498       m_newDocument = false;
1499       m_fileSave->setEnabled(false);
1500       m_detailedView->resetEntryStatus();
1501     } else {
1502       ret = false;
1503     }
1504     m_savingImageLocationChange = false;
1505   }
1506 
1507   StatusBar::self()->clearStatus();
1508   return ret;
1509 }
1510 
1511 void MainWindow::slotFileSaveAsTemplate() {
1512   QScopedPointer<CollectionTemplateDialog> dlg(new CollectionTemplateDialog(this));
1513   if(dlg->exec() != QDialog::Accepted) {
1514     return;
1515   }
1516 
1517   const QString templateName = dlg->templateName();
1518   if(templateName.isEmpty()) {
1519     return;
1520   }
1521   const QString baseName = Tellico::saveLocation(QStringLiteral("collection-templates/")) + templateName;
1522 
1523   // first, save the collection template, which copies the collection fields and filters, but nothing else
1524   const QString collFile = baseName + QLatin1String(".tc");
1525   Data::Document::self()->saveDocumentTemplate(QUrl::fromLocalFile(collFile), templateName);
1526 
1527   // next, save the template descriptions in a config file
1528   const QString specFile = baseName + QLatin1String(".spec");
1529   auto spec = KSharedConfig::openConfig(specFile, KConfig::SimpleConfig)->group(QString());
1530   spec.writeEntry("Name", templateName);
1531   spec.writeEntry("Comment", dlg->templateComment());
1532   spec.writeEntry("Icon", dlg->templateIcon());
1533 }
1534 
1535 void MainWindow::slotFilePrint() {
1536   doPrint(Print);
1537 }
1538 
1539 void MainWindow::slotFilePrintPreview() {
1540   doPrint(PrintPreview);
1541 }
1542 
1543 void MainWindow::doPrint(PrintAction action_) {
1544   slotStatusMsg(i18n("Printing..."));
1545 
1546   // If the collection is being filtered, warn the user
1547   if(m_detailedView->filter()) {
1548     QString str = i18n("The collection is currently being filtered to show a limited subset of "
1549                        "the entries. Only the visible entries will be printed. Continue?");
1550     int ret = KMessageBox::warningContinueCancel(this, str, QString(), KStandardGuiItem::print(),
1551                                                  KStandardGuiItem::cancel(), QStringLiteral("WarnPrintVisible"));
1552     if(ret == KMessageBox::Cancel) {
1553       StatusBar::self()->clearStatus();
1554       return;
1555     }
1556   }
1557 
1558   PrintHandler printHandler(this);
1559   printHandler.setEntries(m_detailedView->visibleEntries());
1560   printHandler.setColumns(m_detailedView->visibleColumns());
1561   if(action_ == Print) {
1562     printHandler.print();
1563   } else {
1564     printHandler.printPreview();
1565   }
1566 
1567   StatusBar::self()->clearStatus();
1568 }
1569 
1570 void MainWindow::slotFileQuit() {
1571   slotStatusMsg(i18n("Exiting..."));
1572 
1573   close(); // will call queryClose()
1574 
1575   StatusBar::self()->clearStatus();
1576 }
1577 
1578 void MainWindow::slotEditCut() {
1579   activateEditSlot("cut()");
1580 }
1581 
1582 void MainWindow::slotEditCopy() {
1583   activateEditSlot("copy()");
1584 }
1585 
1586 void MainWindow::slotEditPaste() {
1587   activateEditSlot("paste()");
1588 }
1589 
1590 void MainWindow::activateEditSlot(const char* slot_) {
1591   // the edit widget is the only one that copies, cuts, and pastes
1592   // the entry view can copy
1593   QWidget* w;
1594   if(m_editDialog->isVisible()) {
1595     w = m_editDialog->focusWidget();
1596   } else {
1597     w = qApp->focusWidget();
1598   }
1599 
1600   while(w && w->isVisible()) {
1601     const QMetaObject* meta = w->metaObject();
1602     const int idx = meta->indexOfSlot(slot_);
1603     if(idx > -1) {
1604 //      myDebug() << "MainWindow invoking" << meta->method(idx).methodSignature();
1605       meta->method(idx).invoke(w, Qt::DirectConnection);
1606       break;
1607     } else {
1608 //      myDebug() << "did not find" << slot_ << "in" << meta->className();
1609       w = qobject_cast<QWidget*>(w->parent());
1610     }
1611   }
1612 }
1613 
1614 void MainWindow::slotEditSelectAll() {
1615   m_detailedView->selectAllVisible();
1616 }
1617 
1618 void MainWindow::slotEditDeselect() {
1619   Controller::self()->slotUpdateSelection(Data::EntryList());
1620 }
1621 
1622 void MainWindow::slotToggleEntryEditor() {
1623   if(m_toggleEntryEditor->isChecked()) {
1624     m_editDialog->show();
1625   } else {
1626     m_editDialog->hide();
1627   }
1628 }
1629 
1630 void MainWindow::slotShowConfigDialog() {
1631   if(!m_configDlg) {
1632     m_configDlg = new ConfigDialog(this);
1633     connect(m_configDlg, &ConfigDialog::signalConfigChanged,
1634             this, &MainWindow::slotHandleConfigChange);
1635     connect(m_configDlg, &QDialog::finished,
1636             this, &MainWindow::slotHideConfigDialog);
1637   } else {
1638     activateDialog(m_configDlg);
1639   }
1640   m_configDlg->show();
1641 }
1642 
1643 void MainWindow::slotHideConfigDialog() {
1644   if(m_configDlg) {
1645     m_configDlg->hide();
1646     m_configDlg->deleteLater();
1647     m_configDlg = nullptr;
1648   }
1649 }
1650 
1651 void MainWindow::slotStatusMsg(const QString& text_) {
1652   m_statusBar->setStatus(text_);
1653 }
1654 
1655 void MainWindow::slotClearStatus() {
1656   StatusBar::self()->clearStatus();
1657 }
1658 
1659 void MainWindow::slotEntryCount() {
1660   Data::CollPtr coll = Data::Document::self()->collection();
1661   if(!coll) {
1662     return;
1663   }
1664 
1665   int count = coll->entryCount();
1666   QString text = i18n("Total entries: %1", count);
1667 
1668   int selectCount = Controller::self()->selectedEntries().count();
1669   int filterCount = m_detailedView->visibleItems();
1670   // if more than one book is selected, add the number of selected books
1671   if(filterCount < count && selectCount > 1) {
1672     text += QLatin1Char(' ');
1673     text += i18n("(%1 filtered; %2 selected)", filterCount, selectCount);
1674   } else if(filterCount < count) {
1675     text += QLatin1Char(' ');
1676     text += i18n("(%1 filtered)", filterCount);
1677   } else if(selectCount > 1) {
1678     text += QLatin1Char(' ');
1679     text += i18n("(%1 selected)", selectCount);
1680   }
1681 
1682   m_statusBar->setCount(text);
1683 }
1684 
1685 void MainWindow::slotEnableOpenedActions() {
1686   slotUpdateToolbarIcons();
1687 
1688   updateCollectionActions();
1689 
1690   // close the filter dialog when a new collection is opened
1691   slotHideFilterDialog();
1692   slotStringMacroDialogFinished();
1693 }
1694 
1695 void MainWindow::slotEnableModifiedActions(bool modified_ /*= true*/) {
1696   updateCaption(modified_);
1697   updateCollectionActions();
1698   m_fileSave->setEnabled(modified_);
1699 }
1700 
1701 void MainWindow::slotHandleConfigChange() {
1702   const int imageLocation = Config::imageLocation();
1703   const bool autoCapitalize = Config::autoCapitalization();
1704   const bool autoFormat = Config::autoFormat();
1705   const QStringList articles = Config::articleList();
1706   const QStringList nocaps = Config::noCapitalizationList();
1707   const QStringList suffixes = Config::nameSuffixList();
1708   const QStringList prefixes = Config::surnamePrefixList();
1709 
1710   m_configDlg->saveConfiguration();
1711 
1712   // only modified if there are entries and image location is changed
1713   if(imageLocation != Config::imageLocation() && !Data::Document::self()->isEmpty()) {
1714     slotImageLocationChanged();
1715   }
1716 
1717   if(autoCapitalize != Config::autoCapitalization() ||
1718     autoFormat != Config::autoFormat() ||
1719     articles != Config::articleList() ||
1720     nocaps != Config::noCapitalizationList() ||
1721     suffixes != Config::nameSuffixList() ||
1722     prefixes != Config::surnamePrefixList()) {
1723     // invalidate all groups
1724     Data::Document::self()->collection()->invalidateGroups();
1725     // refreshing the title causes the group view to refresh
1726     Controller::self()->slotRefreshField(Data::Document::self()->collection()->fieldByName(QStringLiteral("title")));
1727   }
1728 
1729   QString entryXSLTFile = Config::templateName(Kernel::self()->collectionType());
1730   m_entryView->setXSLTFile(entryXSLTFile + QLatin1String(".xsl"));
1731 }
1732 
1733 void MainWindow::slotUpdateCollectionToolBar(Tellico::Data::CollPtr coll_) {
1734   if(!coll_) {
1735     myWarning() << "no collection pointer!";
1736     return;
1737   }
1738 
1739   QString current = m_groupView->groupBy();
1740   if(current.isEmpty() || !coll_->entryGroups().contains(current)) {
1741     current = coll_->defaultGroupField();
1742   }
1743 
1744   const QStringList groups = coll_->entryGroups();
1745   if(groups.isEmpty()) {
1746     m_entryGrouping->clear();
1747     return;
1748   }
1749 
1750   QMap<QString, QString> groupMap; // use a map so they get sorted
1751   foreach(const QString& groupName, groups) {
1752     // special case for people "pseudo-group"
1753     if(groupName == Data::Collection::s_peopleGroupName) {
1754       groupMap.insert(groupName, QLatin1Char('<') + i18n("People") + QLatin1Char('>'));
1755     } else {
1756       groupMap.insert(groupName, coll_->fieldTitleByName(groupName));
1757     }
1758   }
1759 
1760   const QStringList titles = groupMap.values();
1761   if(titles == m_entryGrouping->items()) {
1762     // no need to update anything
1763     return;
1764   }
1765   const QStringList names = groupMap.keys();
1766   int index = names.indexOf(current);
1767   if(index == -1) {
1768     current = names[0];
1769     index = 0;
1770   }
1771   m_entryGrouping->setItems(titles);
1772   m_entryGrouping->setCurrentItem(index);
1773   // in case the current grouping field get modified to be non-grouping...
1774   m_groupView->setGroupField(current); // don't call slotChangeGrouping() since it adds an undo item
1775 
1776   // TODO::I have no idea how to get the combobox to update its size
1777   // this is the hackiest of hacks, taken from KXmlGuiWindow::saveNewToolbarConfig()
1778   // the window flickers as toolbar resizes, unavoidable?
1779   // crashes if removeClient//addClient is called here, need to do later in event loop
1780   QTimer::singleShot(0, this, &MainWindow::guiFactoryReset);
1781 }
1782 
1783 void MainWindow::slotChangeGrouping() {
1784   const QString title = m_entryGrouping->currentText();
1785 
1786   QString groupName = Data::Document::self()->collection()->fieldNameByTitle(title);
1787   if(groupName.isEmpty()) {
1788     if(title == (QLatin1Char('<') + i18n("People") + QLatin1Char('>'))) {
1789       groupName = Data::Collection::s_peopleGroupName;
1790     } else {
1791       groupName = Data::Document::self()->collection()->defaultGroupField();
1792     }
1793   }
1794   m_groupView->setGroupField(groupName);
1795   m_viewTabs->setCurrentWidget(m_groupView);
1796 }
1797 
1798 void MainWindow::slotShowReportDialog() {
1799   if(!m_reportDlg) {
1800     m_reportDlg = new ReportDialog(this);
1801     connect(m_reportDlg, &QDialog::finished,
1802             this, &MainWindow::slotHideReportDialog);
1803   } else {
1804     activateDialog(m_reportDlg);
1805   }
1806   m_reportDlg->show();
1807 }
1808 
1809 void MainWindow::slotHideReportDialog() {
1810   if(m_reportDlg) {
1811     m_reportDlg->hide();
1812     m_reportDlg->deleteLater();
1813     m_reportDlg = nullptr;
1814   }
1815 }
1816 
1817 void MainWindow::XSLTError() {
1818   QString str = i18n("Tellico encountered an error in XSLT processing.") + QLatin1Char('\n');
1819   str += i18n("Please check your installation.");
1820   Kernel::self()->sorry(str);
1821 }
1822 
1823 void MainWindow::slotShowFilterDialog() {
1824   if(!m_filterDlg) {
1825     m_filterDlg = new FilterDialog(FilterDialog::CreateFilter, this); // allow saving
1826     m_quickFilter->setEnabled(false);
1827     connect(m_filterDlg, &FilterDialog::signalCollectionModified,
1828             Data::Document::self(), &Data::Document::slotSetModified);
1829     connect(m_filterDlg, &FilterDialog::signalUpdateFilter,
1830             this, &MainWindow::slotUpdateFilter);
1831     connect(m_filterDlg, &QDialog::finished,
1832             this, &MainWindow::slotHideFilterDialog);
1833   } else {
1834     activateDialog(m_filterDlg);
1835   }
1836   m_filterDlg->setFilter(m_detailedView->filter());
1837   m_filterDlg->show();
1838 }
1839 
1840 void MainWindow::slotHideFilterDialog() {
1841 //  m_quickFilter->blockSignals(false);
1842   m_quickFilter->setEnabled(true);
1843   if(m_filterDlg) {
1844     m_filterDlg->hide();
1845     m_filterDlg->deleteLater();
1846     m_filterDlg = nullptr;
1847   }
1848 }
1849 
1850 void MainWindow::slotQueueFilter() {
1851   if(m_dontQueueFilter) {
1852     return;
1853   }
1854   m_queuedFilters++;
1855   QTimer::singleShot(200, this, &MainWindow::slotCheckFilterQueue);
1856 }
1857 
1858 void MainWindow::slotCheckFilterQueue() {
1859   m_queuedFilters--;
1860   if(m_queuedFilters > 0) {
1861     return;
1862   }
1863 
1864   setFilter(m_quickFilter->text());
1865 }
1866 
1867 void MainWindow::slotUpdateFilter(FilterPtr filter_) {
1868   // Can't just block signals because clear button won't show then
1869   m_dontQueueFilter = true;
1870   if(filter_) {
1871     // for a saved filter, show the filter name and a leading icon
1872     if(m_quickFilter->actions().isEmpty()) {
1873       m_quickFilter->addAction(QIcon::fromTheme(QStringLiteral("view-filter")), QLineEdit::LeadingPosition);
1874     }
1875     m_quickFilter->setText(QLatin1Char('<') + filter_->name() + QLatin1Char('>'));
1876   } else {
1877     m_quickFilter->setText(QStringLiteral(" ")); // To be able to clear custom filter
1878   }
1879   Controller::self()->slotUpdateFilter(filter_);
1880   m_dontQueueFilter = false;
1881 }
1882 
1883 void MainWindow::setFilter(const QString& text_) {
1884   // might have an "action" associated if a saved filter was displayed
1885   auto actions = m_quickFilter->actions();
1886   if(!actions.isEmpty()) {
1887     // clear all of the saved filter name
1888     slotClearFilter();
1889     return;
1890   }
1891   QString text = text_.trimmed();
1892   FilterPtr filter;
1893   if(!text.isEmpty()) {
1894     filter = new Filter(Filter::MatchAll);
1895     QString fieldName; // empty field name means match on any field
1896     // if the text contains '=' assume it's a field name or title
1897     if(text.indexOf(QLatin1Char('=')) > -1) {
1898       fieldName = text.section(QLatin1Char('='), 0, 0).trimmed();
1899       text = text.section(QLatin1Char('='), 1).trimmed();
1900       // check that the field name might be a title
1901       if(!Data::Document::self()->collection()->hasField(fieldName)) {
1902         fieldName = Data::Document::self()->collection()->fieldNameByTitle(fieldName);
1903       }
1904     }
1905     Filter::populateQuickFilter(filter, fieldName, text, Config::quickFilterRegExp());
1906     // also want to update the line edit in case the filter was set by DBUS
1907     if(m_quickFilter->text() != text_) {
1908       m_quickFilter->setText(text_);
1909     }
1910   }
1911   // only update filter if one exists or did exist
1912   if(filter || m_detailedView->filter()) {
1913     Controller::self()->slotUpdateFilter(filter);
1914   }
1915 }
1916 
1917 void MainWindow::slotShowCollectionFieldsDialog() {
1918   if(!m_collFieldsDlg) {
1919     m_collFieldsDlg = new CollectionFieldsDialog(Data::Document::self()->collection(), this);
1920     m_collFieldsDlg->setNotifyKernel(true);
1921     connect(m_collFieldsDlg, &CollectionFieldsDialog::beginCommandGroup,
1922             Kernel::self(), &Kernel::beginCommandGroup);
1923     connect(m_collFieldsDlg, &CollectionFieldsDialog::endCommandGroup,
1924             Kernel::self(), &Kernel::endCommandGroup);
1925     connect(m_collFieldsDlg, &CollectionFieldsDialog::addField,
1926             Kernel::self(), &Kernel::addField);
1927     connect(m_collFieldsDlg, &CollectionFieldsDialog::modifyField,
1928             Kernel::self(), &Kernel::modifyField);
1929     connect(m_collFieldsDlg, &CollectionFieldsDialog::removeField,
1930             Kernel::self(), &Kernel::removeField);
1931     connect(m_collFieldsDlg, &CollectionFieldsDialog::reorderFields,
1932             Kernel::self(), &Kernel::reorderFields);
1933     connect(m_collFieldsDlg, &QDialog::finished,
1934             this, &MainWindow::slotHideCollectionFieldsDialog);
1935   } else {
1936     activateDialog(m_collFieldsDlg);
1937   }
1938   m_collFieldsDlg->show();
1939 }
1940 
1941 void MainWindow::slotHideCollectionFieldsDialog() {
1942   if(m_collFieldsDlg) {
1943     m_collFieldsDlg->hide();
1944     m_collFieldsDlg->deleteLater();
1945     m_collFieldsDlg = nullptr;
1946   }
1947 }
1948 
1949 void MainWindow::slotFileImport(int format_) {
1950   slotStatusMsg(i18n("Importing data..."));
1951   m_quickFilter->clear();
1952 
1953   Import::Format format = static_cast<Import::Format>(format_);
1954   bool checkURL = true;
1955   QUrl url;
1956   switch(ImportDialog::importTarget(format)) {
1957     case Import::File:
1958       {
1959         QString fileClass;
1960         const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///import")), fileClass);
1961         url = QFileDialog::getOpenFileUrl(this, i18n("Import File"), startUrl, ImportDialog::fileFilter(format));
1962         KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1963       }
1964       break;
1965 
1966     case Import::Dir:
1967       // TODO: allow remote audiofile importing
1968       {
1969         const QString fileClass(QStringLiteral("ImportDir"));
1970         QString dirName = ImportDialog::startDir(format);
1971         if(dirName.isEmpty()) {
1972           dirName = KRecentDirs::dir(fileClass);
1973         }
1974         QString chosenDir = QFileDialog::getExistingDirectory(this, i18n("Import Directory"), dirName);
1975         url = QUrl::fromLocalFile(chosenDir);
1976         KRecentDirs::add(fileClass, chosenDir);
1977       }
1978       break;
1979 
1980     case Import::None:
1981     default:
1982       checkURL = false;
1983       break;
1984   }
1985 
1986   if(checkURL) {
1987     bool ok = !url.isEmpty() && url.isValid() && QFile::exists(url.toLocalFile());
1988     if(!ok) {
1989       StatusBar::self()->clearStatus();
1990       return;
1991     }
1992   }
1993   importFile(format, QList<QUrl>() << url);
1994   StatusBar::self()->clearStatus();
1995 }
1996 
1997 void MainWindow::slotFileExport(int format_) {
1998   slotStatusMsg(i18n("Exporting data..."));
1999 
2000   Export::Format format = static_cast<Export::Format>(format_);
2001   ExportDialog dlg(format, Data::Document::self()->collection(), this);
2002 
2003   if(dlg.exec() == QDialog::Rejected) {
2004     StatusBar::self()->clearStatus();
2005     return;
2006   }
2007 
2008   switch(ExportDialog::exportTarget(format)) {
2009     case Export::None:
2010       dlg.exportURL();
2011       break;
2012 
2013     case Export::Dir:
2014       myDebug() << "ExportDir not implemented!";
2015       break;
2016 
2017     case Export::File:
2018     {
2019       QString fileClass;
2020       const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///export")), fileClass);
2021       QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Export As"), startUrl, dlg.fileFilter());
2022       if(url.isEmpty()) {
2023         StatusBar::self()->clearStatus();
2024         return;
2025       }
2026 
2027       if(url.isValid()) {
2028         if(url.isLocalFile()) {
2029           KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
2030         }
2031         GUI::CursorSaver cs(Qt::WaitCursor);
2032         dlg.exportURL(url);
2033       }
2034     }
2035     break;
2036   }
2037 
2038   StatusBar::self()->clearStatus();
2039 }
2040 
2041 void MainWindow::slotShowStringMacroDialog() {
2042   if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) {
2043     return;
2044   }
2045 
2046   if(!m_stringMacroDlg) {
2047     const Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data());
2048     m_stringMacroDlg = new StringMapDialog(c->macroList(), this, false);
2049     m_stringMacroDlg->setWindowTitle(i18n("String Macros"));
2050     m_stringMacroDlg->setLabels(i18n("Macro"), i18n("String"));
2051     connect(m_stringMacroDlg, &QDialog::finished, this, &MainWindow::slotStringMacroDialogFinished);
2052   } else {
2053     activateDialog(m_stringMacroDlg);
2054   }
2055   m_stringMacroDlg->show();
2056 }
2057 
2058 void MainWindow::slotStringMacroDialogFinished(int result_) {
2059   // no point in checking if collection is bibtex, as dialog would never have been created
2060   if(!m_stringMacroDlg) {
2061     return;
2062   }
2063   if(result_ == QDialog::Accepted) {
2064     static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data())->setMacroList(m_stringMacroDlg->stringMap());
2065     Data::Document::self()->setModified(true);
2066   }
2067   m_stringMacroDlg->hide();
2068   m_stringMacroDlg->deleteLater();
2069   m_stringMacroDlg = nullptr;
2070 }
2071 
2072 void MainWindow::slotShowBibtexKeyDialog() {
2073   if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) {
2074     return;
2075   }
2076 
2077   if(!m_bibtexKeyDlg) {
2078     m_bibtexKeyDlg = new BibtexKeyDialog(Data::Document::self()->collection(), this);
2079     connect(m_bibtexKeyDlg, &QDialog::finished, this, &MainWindow::slotHideBibtexKeyDialog);
2080     connect(m_bibtexKeyDlg, &BibtexKeyDialog::signalUpdateFilter,
2081             this, &MainWindow::slotUpdateFilter);
2082   } else {
2083     activateDialog(m_bibtexKeyDlg);
2084   }
2085   m_bibtexKeyDlg->show();
2086 }
2087 
2088 void MainWindow::slotHideBibtexKeyDialog() {
2089   if(m_bibtexKeyDlg) {
2090     m_bibtexKeyDlg->deleteLater();
2091     m_bibtexKeyDlg = nullptr;
2092   }
2093 }
2094 
2095 void MainWindow::slotNewEntry() {
2096   m_toggleEntryEditor->setChecked(true);
2097   slotToggleEntryEditor();
2098   m_editDialog->slotHandleNew();
2099 }
2100 
2101 void MainWindow::slotEditDialogFinished() {
2102   m_toggleEntryEditor->setChecked(false);
2103 }
2104 
2105 void MainWindow::slotShowEntryEditor() {
2106   m_toggleEntryEditor->setChecked(true);
2107   m_editDialog->show();
2108   activateDialog(m_editDialog);
2109 }
2110 
2111 void MainWindow::slotConvertToBibliography() {
2112   // only book collections can be converted to bibtex
2113   Data::CollPtr coll = Data::Document::self()->collection();
2114   if(!coll || coll->type() != Data::Collection::Book) {
2115     return;
2116   }
2117 
2118   GUI::CursorSaver cs;
2119 
2120   // need to make sure all images are transferred
2121   Data::Document::self()->loadAllImagesNow();
2122 
2123   Data::CollPtr newColl = Data::BibtexCollection::convertBookCollection(coll);
2124   if(newColl) {
2125     m_newDocument = true;
2126     Kernel::self()->replaceCollection(newColl);
2127     m_fileOpenRecent->setCurrentItem(-1);
2128     slotUpdateToolbarIcons();
2129     updateCollectionActions();
2130   } else {
2131     myWarning() << "ERROR: no bibliography created!";
2132   }
2133 }
2134 
2135 void MainWindow::slotCiteEntry(int action_) {
2136   StatusBar::self()->setStatus(i18n("Creating citations..."));
2137   Cite::ActionManager* man = Cite::ActionManager::self();
2138   man->cite(static_cast<Cite::CiteAction>(action_), Controller::self()->selectedEntries());
2139   if(man->hasError()) {
2140     Kernel::self()->sorry(man->errorString());
2141   }
2142   StatusBar::self()->clearStatus();
2143 }
2144 
2145 void MainWindow::slotShowFetchDialog() {
2146   if(!m_fetchDlg) {
2147     m_fetchDlg = new FetchDialog(this);
2148     connect(m_fetchDlg, &QDialog::finished, this, &MainWindow::slotHideFetchDialog);
2149     connect(Controller::self(), &Controller::collectionAdded, m_fetchDlg, &FetchDialog::slotResetCollection);
2150   } else {
2151     activateDialog(m_fetchDlg);
2152   }
2153   m_fetchDlg->show();
2154 }
2155 
2156 void MainWindow::slotHideFetchDialog() {
2157   if(m_fetchDlg) {
2158     m_fetchDlg->hide();
2159     m_fetchDlg->deleteLater();
2160     m_fetchDlg = nullptr;
2161   }
2162 }
2163 
2164 bool MainWindow::importFile(Tellico::Import::Format format_, const QUrl& url_, Tellico::Import::Action action_) {
2165   // try to open document
2166   GUI::CursorSaver cs(Qt::WaitCursor);
2167 
2168   bool failed = false;
2169   Data::CollPtr coll;
2170   if(!url_.isEmpty() && url_.isValid() && NetAccess::exists(url_, true, this)) {
2171     coll = ImportDialog::importURL(format_, url_);
2172   } else {
2173     Kernel::self()->sorry(i18n(errorLoad, url_.fileName()));
2174     failed = true;
2175   }
2176 
2177   if(!coll && !m_initialized) {
2178     // special case on startup when openURL() is called with a command line argument
2179     // and that URL can't be opened. The window still needs to be initialized
2180     // the doc object is created with an initial book collection, continue with that
2181     Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
2182     m_fileSave->setEnabled(false);
2183     slotEnableOpenedActions();
2184     slotEnableModifiedActions(false);
2185     slotEntryCount();
2186     m_fileOpenRecent->setCurrentItem(-1);
2187     m_initialized = true;
2188     failed = true;
2189   } else if(coll) {
2190     // this is rather dumb, but I'm too lazy to find the bug
2191     // if the document isn't initialized, then Tellico crashes
2192     // since Document::replaceCollection() ends up calling lots of stuff that isn't initialized
2193     if(!m_initialized) {
2194       Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
2195       m_initialized = true;
2196     }
2197     failed = !importCollection(coll, action_);
2198   }
2199 
2200   StatusBar::self()->clearStatus();
2201   return !failed; // return true means success
2202 }
2203 
2204 bool MainWindow::exportCollection(Tellico::Export::Format format_, const QUrl& url_, bool filtered_) {
2205   if(!url_.isValid()) {
2206     myDebug() << "invalid URL:" << url_;
2207     return false;
2208   }
2209 
2210   GUI::CursorSaver cs;
2211   const Data::CollPtr coll = Data::Document::self()->collection();
2212   if(!coll) {
2213     return false;
2214   }
2215 
2216   // only bibliographies can export to bibtex or bibtexml
2217   const bool isBibtex = (coll->type() == Data::Collection::Bibtex);
2218   if(!isBibtex && (format_ == Export::Bibtex || format_ == Export::Bibtexml)) {
2219     return false;
2220   }
2221   // only books and bibliographies can export to alexandria
2222   const bool isBook = (coll->type() == Data::Collection::Book);
2223   if(!isBibtex && !isBook && format_ == Export::Alexandria) {
2224     return false;
2225   }
2226 
2227   return ExportDialog::exportCollection(coll, filtered_ ? Controller::self()->visibleEntries() : coll->entries(),
2228                                         format_, url_);
2229 }
2230 
2231 bool MainWindow::showEntry(Data::ID id) {
2232   Data::EntryPtr entry = Data::Document::self()->collection()->entryById(id);
2233   if(entry) {
2234     m_entryView->showEntry(entry);
2235   }
2236   return entry;
2237 }
2238 
2239 void MainWindow::addFilterView() {
2240   if(m_filterView) {
2241     return;
2242   }
2243 
2244   m_filterView = new FilterView(m_viewTabs);
2245   Controller::self()->addObserver(m_filterView);
2246   m_viewTabs->insertTab(1, m_filterView, QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Filters"));
2247   m_filterView->setWhatsThis(i18n("<qt>The <i>Filter View</i> shows the entries which meet certain "
2248                                   "filter rules.</qt>"));
2249 
2250   connect(m_filterView, &FilterView::signalUpdateFilter,
2251           this, &MainWindow::slotUpdateFilter);
2252   // use the EntrySelectionModel as a proxy so when entries get selected in the filter view
2253   // the edit dialog and entry view are updated
2254   // TODO: consider using KSelectionProxyModel
2255   static_cast<EntrySelectionModel*>(m_iconView->selectionModel())->addSelectionProxy(m_filterView->selectionModel());
2256 
2257   // sort by count if column = 1
2258   int sortRole = Config::filterViewSortColumn() == 0 ? static_cast<int>(Qt::DisplayRole) : static_cast<int>(RowCountRole);
2259   Qt::SortOrder sortOrder = Config::filterViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder;
2260   m_filterView->setSorting(sortOrder, sortRole);
2261 }
2262 
2263 void MainWindow::addLoanView() {
2264   if(m_loanView) {
2265     return;
2266   }
2267 
2268   m_loanView = new LoanView(m_viewTabs);
2269   Controller::self()->addObserver(m_loanView);
2270   m_viewTabs->insertTab(2, m_loanView, QIcon::fromTheme(QStringLiteral("kaddressbook")), i18n("Loans"));
2271   m_loanView->setWhatsThis(i18n("<qt>The <i>Loan View</i> shows a list of all the people who "
2272                                 "have borrowed items from your collection.</qt>"));
2273 
2274   // use the EntrySelectionModel as a proxy so when entries get selected in the loan view
2275   // the edit dialog and entry view are updated
2276   // TODO: consider using KSelectionProxyModel
2277   static_cast<EntrySelectionModel*>(m_iconView->selectionModel())->addSelectionProxy(m_loanView->selectionModel());
2278 
2279   // sort by count if column = 1
2280   int sortRole = Config::loanViewSortColumn() == 0 ? static_cast<int>(Qt::DisplayRole) : static_cast<int>(RowCountRole);
2281   Qt::SortOrder sortOrder = Config::loanViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder;
2282   m_loanView->setSorting(sortOrder, sortRole);
2283 }
2284 
2285 void MainWindow::updateCaption(bool modified_) {
2286   QString caption;
2287   if(Data::Document::self()->collection()) {
2288     caption = Data::Document::self()->collection()->title();
2289   }
2290   if(!m_newDocument) {
2291     if(!caption.isEmpty()) {
2292        caption += QLatin1String(" - ");
2293     }
2294     QUrl u = Data::Document::self()->URL();
2295     if(u.isLocalFile() && u.fileName() == i18n(Tellico::untitledFilename)) {
2296       // for new files, the filename is set to Untitled in Data::Document
2297       caption += u.fileName();
2298     } else {
2299       caption += u.toDisplayString(QUrl::PreferLocalFile);
2300     }
2301   }
2302   setCaption(caption, modified_);
2303 }
2304 
2305 void MainWindow::slotUpdateToolbarIcons() {
2306   // first change the icon for the menu item
2307   if(Kernel::self()->collectionType() == Data::Collection::Base) {
2308     m_newEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
2309   } else {
2310     m_newEntry->setIcon(QIcon(QLatin1String(":/icons/") + Kernel::self()->collectionTypeName()));
2311   }
2312 }
2313 
2314 void MainWindow::slotGroupLabelActivated() {
2315   // need entry grouping combo id
2316   foreach(QWidget* widget, m_entryGrouping->associatedWidgets()) {
2317     if(::qobject_cast<KToolBar*>(widget)) {
2318       QWidget* container = m_entryGrouping->requestWidget(widget);
2319       QComboBox* combo = ::qobject_cast<QComboBox*>(container); //krazy:exclude=qclasses
2320       if(combo) {
2321         combo->showPopup();
2322         break;
2323       }
2324     }
2325   }
2326 }
2327 
2328 void MainWindow::slotFilterLabelActivated() {
2329   m_quickFilter->setFocus();
2330   m_quickFilter->selectAll();
2331 }
2332 
2333 void MainWindow::slotClearFilter() {
2334   auto actions = m_quickFilter->actions();
2335   if(!actions.isEmpty()) {
2336     m_quickFilter->removeAction(actions.first());
2337   }
2338   m_quickFilter->clear();
2339   slotQueueFilter();
2340 }
2341 
2342 void MainWindow::slotRenameCollection() {
2343   Kernel::self()->renameCollection();
2344 }
2345 
2346 void MainWindow::slotImageLocationMismatch() {
2347   // TODO: having a single image location mismatch should not be reason to completely save the whole document
2348   QTimer::singleShot(0, this, &MainWindow::slotImageLocationChanged);
2349 }
2350 
2351 void MainWindow::slotImageLocationChanged() {
2352   if(m_savingImageLocationChange) {
2353     return;
2354   }
2355   m_savingImageLocationChange = true;
2356   Data::Document::self()->slotSetModified();
2357   KMessageBox::information(this, QLatin1String("<qt>") +
2358                                  i18n("Some images are not saved in the configured location. The current file "
2359                                       "must be saved and the images will be transferred to the new location.") +
2360                                  QLatin1String("</qt>"));
2361   fileSave();
2362   m_savingImageLocationChange = false;
2363 }
2364 
2365 void MainWindow::updateCollectionActions() {
2366   if(!Data::Document::self()->collection()) {
2367     return;
2368   }
2369 
2370   stateChanged(QStringLiteral("collection_reset"));
2371 
2372   Data::Collection::Type type = Data::Document::self()->collection()->type();
2373   stateChanged(QLatin1String("is_") + CollectionFactory::typeName(type));
2374 
2375   Controller::self()->updateActions();
2376   // special case when there are no available data sources
2377   if(m_fetchActions.isEmpty() && m_updateAll) {
2378     m_updateAll->setEnabled(false);
2379   }
2380 }
2381 
2382 void MainWindow::updateEntrySources() {
2383   const QString actionListName = QStringLiteral("update_entry_actions");
2384   unplugActionList(actionListName);
2385   foreach(QAction* action, m_fetchActions) {
2386     foreach(QWidget* widget, action->associatedWidgets()) {
2387       widget->removeAction(action);
2388     }
2389     m_updateMapper->removeMappings(action);
2390   }
2391   qDeleteAll(m_fetchActions);
2392   m_fetchActions.clear();
2393 
2394   void (QAction::* triggeredBool)(bool) = &QAction::triggered;
2395   void (QSignalMapper::* mapVoid)() = &QSignalMapper::map;
2396   Fetch::FetcherVec vec = Fetch::Manager::self()->fetchers(Kernel::self()->collectionType());
2397   foreach(Fetch::Fetcher::Ptr fetcher, vec) {
2398     QAction* action = new QAction(Fetch::Manager::fetcherIcon(fetcher.data()), fetcher->source(), actionCollection());
2399     action->setToolTip(i18n("Update entry data from %1", fetcher->source()));
2400     connect(action, triggeredBool, m_updateMapper, mapVoid);
2401     m_updateMapper->setMapping(action, fetcher->source());
2402     m_fetchActions.append(action);
2403   }
2404 
2405   plugActionList(actionListName, m_fetchActions);
2406 }
2407 
2408 void MainWindow::importFile(Tellico::Import::Format format_, const QList<QUrl>& urls_) {
2409   QList<QUrl> urls = urls_;
2410   // update as DropHandler and Importer classes are updated
2411   if(urls_.count() > 1 &&
2412      format_ != Import::Bibtex &&
2413      format_ != Import::RIS &&
2414      format_ != Import::CIW &&
2415      format_ != Import::PDF) {
2416     QUrl u = urls_.front();
2417     QString url = u.isLocalFile() ? u.path() : u.toDisplayString();
2418     Kernel::self()->sorry(i18n("Tellico can only import one file of this type at a time. "
2419                                "Only %1 will be imported.", url));
2420     urls.clear();
2421     urls += u;
2422   }
2423 
2424   ImportDialog dlg(format_, urls, this);
2425   if(dlg.exec() != QDialog::Accepted) {
2426     return;
2427   }
2428 
2429 //  if edit dialog is saved ok and if replacing, then the doc is saved ok
2430   if(m_editDialog->queryModified() &&
2431      (dlg.action() != Import::Replace || querySaveModified())) {
2432     GUI::CursorSaver cs(Qt::WaitCursor);
2433     Data::CollPtr coll = dlg.collection();
2434     if(!coll) {
2435       if(!dlg.statusMessage().isEmpty()) {
2436         Kernel::self()->sorry(dlg.statusMessage());
2437       }
2438       return;
2439     }
2440     importCollection(coll, dlg.action());
2441   }
2442 }
2443 
2444 void MainWindow::importText(Tellico::Import::Format format_, const QString& text_) {
2445   if(text_.isEmpty()) {
2446     return;
2447   }
2448   Data::CollPtr coll = ImportDialog::importText(format_, text_);
2449   if(coll) {
2450     importCollection(coll, Import::Merge);
2451   }
2452 }
2453 
2454 bool MainWindow::importCollection(Tellico::Data::CollPtr coll_, Tellico::Import::Action action_) {
2455   bool failed = false;
2456   switch(action_) {
2457     case Import::Append:
2458       {
2459         // only append if match, but special case importing books into bibliographies
2460         Data::CollPtr c = Data::Document::self()->collection();
2461         if(c->type() == coll_->type()
2462           || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) {
2463           Kernel::self()->appendCollection(coll_);
2464           slotEnableModifiedActions(true);
2465         } else {
2466           Kernel::self()->sorry(i18n(errorAppendType));
2467           failed = true;
2468         }
2469       }
2470       break;
2471 
2472     case Import::Merge:
2473       {
2474         // only merge if match, but special case importing books into bibliographies
2475         Data::CollPtr c = Data::Document::self()->collection();
2476         if(c->type() == coll_->type()
2477           || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) {
2478           Kernel::self()->mergeCollection(coll_);
2479           slotEnableModifiedActions(true);
2480         } else {
2481           Kernel::self()->sorry(i18n(errorMergeType));
2482           failed = true;
2483         }
2484       }
2485       break;
2486 
2487     default: // replace
2488       Kernel::self()->replaceCollection(coll_);
2489       m_fileOpenRecent->setCurrentItem(-1);
2490       m_newDocument = true;
2491       slotEnableOpenedActions();
2492       slotEnableModifiedActions(false);
2493       break;
2494   }
2495   // tell the entry views and models that there are no further images to load
2496   m_detailedView->slotRefreshImages();
2497   return !failed;
2498 }
2499 
2500 void MainWindow::slotURLAction(const QUrl& url_) {
2501   Q_ASSERT(url_.scheme() == QLatin1String("tc"));
2502   QString actionName = url_.fileName();
2503   QAction* action = this->action(actionName.toLatin1().constData());
2504   if(action) {
2505     action->activate(QAction::Trigger);
2506   } else {
2507     myWarning() << "unknown action: " << actionName;
2508   }
2509 }
2510 
2511 bool MainWindow::eventFilter(QObject* obj_, QEvent* ev_) {
2512   if(ev_->type() == QEvent::KeyPress && obj_ == m_quickFilter) {
2513     switch(static_cast<QKeyEvent*>(ev_)->key()) {
2514       case Qt::Key_Escape:
2515         m_quickFilter->clear();
2516         return true;
2517     }
2518   }
2519   return KXmlGuiWindow::eventFilter(obj_, ev_);
2520 }
2521 
2522 void MainWindow::slotToggleFullScreen() {
2523   Qt::WindowStates ws = windowState();
2524   setWindowState((ws & Qt::WindowFullScreen) ? (ws & ~Qt::WindowFullScreen) : (ws | Qt::WindowFullScreen));
2525 }
2526 
2527 void MainWindow::slotToggleMenuBarVisibility() {
2528   QMenuBar* mb = menuBar();
2529   mb->isHidden() ? mb->show() : mb->hide();
2530 }
2531 
2532 void MainWindow::slotToggleLayoutLock(bool lock_) {
2533   m_groupViewDock->setLocked(lock_);
2534   m_collectionViewDock->setLocked(lock_);
2535 }
2536 
2537 void MainWindow::slotResetLayout() {
2538   removeDockWidget(m_groupViewDock);
2539   addDockWidget(Qt::LeftDockWidgetArea, m_groupViewDock);
2540   m_groupViewDock->show();
2541 
2542   m_dummyWindow->removeDockWidget(m_collectionViewDock);
2543   m_dummyWindow->addDockWidget(Qt::TopDockWidgetArea, m_collectionViewDock);
2544   m_collectionViewDock->show();
2545 }
2546 
2547 void MainWindow::guiFactoryReset() {
2548   guiFactory()->removeClient(this);
2549   guiFactory()->reset();
2550   guiFactory()->addClient(this);
2551 
2552   // set up custom actions for collection templates, have to do this AFTER createGUI() or factory() reset
2553   const QString actionListName = QStringLiteral("collection_template_list");
2554   unplugActionList(actionListName);
2555   QSignalMapper* collectionTemplateMapper = new QSignalMapper(this);
2556 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
2557   void (QSignalMapper::* mappedString)(QString) = &QSignalMapper::mapped;
2558   connect(collectionTemplateMapper, mappedString, this, &MainWindow::slotFileNewByTemplate);
2559 #else
2560   connect(collectionTemplateMapper, &QSignalMapper::mappedString, this, &MainWindow::slotFileNewByTemplate);
2561 #endif
2562 
2563   void (QAction::* triggeredBool)(bool) = &QAction::triggered;
2564   void (QSignalMapper::* mapVoid)() = &QSignalMapper::map;
2565   QList<QAction*> coll_actions;
2566   const QStringList customCollections = Tellico::locateAllFiles(QStringLiteral("tellico/collection-templates/*.tc"));
2567   if(!customCollections.isEmpty()) {
2568     m_newCollectionMenu->addSeparator();
2569   }
2570   foreach(const QString& collectionFile, customCollections) {
2571     QFileInfo info(collectionFile);
2572     auto action = new QAction(info.completeBaseName(), actionCollection());
2573     connect(action, triggeredBool, collectionTemplateMapper, mapVoid);
2574     const QString specFile = info.canonicalPath() + QDir::separator() + info.completeBaseName() + QLatin1String(".spec");
2575     if(QFileInfo::exists(specFile)) {
2576       KConfig config(specFile, KConfig::SimpleConfig);
2577       const KConfigGroup cg = config.group(QString());
2578       action->setText(cg.readEntry("Name", info.completeBaseName()));
2579       action->setToolTip(cg.readEntry("Comment"));
2580       action->setIcon(QIcon::fromTheme(cg.readEntry("Icon"), QIcon::fromTheme(QStringLiteral("document-new"))));
2581     } else {
2582       myDebug() << "No spec file for" << info.completeBaseName();
2583       action->setText(info.completeBaseName());
2584       action->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
2585     }
2586     collectionTemplateMapper->setMapping(action, collectionFile);
2587     coll_actions.append(action);
2588     m_newCollectionMenu->addAction(action);
2589   }
2590   plugActionList(actionListName, coll_actions);
2591 }
2592 
2593 void MainWindow::showLog() {
2594   auto dlg = new QDialog(this);
2595   auto layout = new QVBoxLayout();
2596   dlg->setLayout(layout);
2597   dlg->setWindowTitle(i18nc("@title:window", "Tellico Log"));
2598 
2599   auto viewer = new QTextEdit(dlg);
2600   viewer->setWordWrapMode(QTextOption::NoWrap);
2601   viewer->setReadOnly(true);
2602   viewer->setStyleSheet(QStringLiteral("QTextEdit { font-family: monospace; }"));
2603   layout->addWidget(viewer);
2604 
2605   auto buttonBox = new QDialogButtonBox(dlg);
2606   buttonBox->setStandardButtons(QDialogButtonBox::Close);
2607   connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
2608   layout->addWidget(buttonBox);
2609 
2610   // TODO:: monitor for updates and reload log?
2611   auto logFile = Logger::self()->logFile();
2612   if(!logFile.isEmpty()) {
2613     Logger::self()->flush();
2614     QFile file(logFile);
2615     if(file.open(QFile::ReadOnly | QIODevice::Text)) {
2616       QTextStream in(&file);
2617       viewer->setPlainText(in.readAll());
2618     }
2619   }
2620 
2621   dlg->setMinimumSize(800, 600);
2622   dlg->setAttribute(Qt::WA_DeleteOnClose, true);
2623   dlg->show();
2624 }