1 /***************************************************************************
2     Copyright (C) 2001-2020 Robby Stephenson <robby@periapsis.org>
3     Copyright (C) 2011 Pedro Miguel Carvalho <kde@pmc.com.pt>
4  ***************************************************************************/
5 
6 /***************************************************************************
7  *                                                                         *
8  *   This program is free software; you can redistribute it and/or         *
9  *   modify it under the terms of the GNU General Public License as        *
10  *   published by the Free Software Foundation; either version 2 of        *
11  *   the License or (at your option) version 3 or any later version        *
12  *   accepted by the membership of KDE e.V. (or its successor approved     *
13  *   by the membership of KDE e.V.), which shall act as a proxy            *
14  *   defined in Section 14 of version 3 of the license.                     *
15  *                                                                         *
16  *   This program is distributed in the hope that it will be useful,       *
17  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
18  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
19  *   GNU General Public License for more details.                          *
20  *                                                                         *
21  *   You should have received a copy of the GNU General Public License     *
22  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
23  *                                                                         *
24  ***************************************************************************/
25 
26 #include "mainwindow.h"
27 #include "tellico_kernel.h"
28 #include "document.h"
29 #include "detailedlistview.h"
30 #include "entryeditdialog.h"
31 #include "groupview.h"
32 #include "viewstack.h"
33 #include "collection.h"
34 #include "collectionfactory.h"
35 #include "entry.h"
36 #include "configdialog.h"
37 #include "filter.h"
38 #include "filterdialog.h"
39 #include "collectionfieldsdialog.h"
40 #include "controller.h"
41 #include "importdialog.h"
42 #include "exportdialog.h"
43 #include "core/filehandler.h" // needed so static mainWindow variable can be set
44 #include "printhandler.h"
45 #include "entryview.h"
46 #include "entryiconview.h"
47 #include "images/imagefactory.h" // needed so tmp files can get cleaned
48 #include "collections/collectioninitializer.h"
49 #include "collections/bibtexcollection.h" // needed for bibtex string macro dialog
50 #include "utils/bibtexhandler.h" // needed for bibtex options
51 #include "utils/datafileregistry.h"
52 #include "fetchdialog.h"
53 #include "reportdialog.h"
54 #include "bibtexkeydialog.h"
55 #include "core/tellico_strings.h"
56 #include "filterview.h"
57 #include "loanview.h"
58 #include "fetch/fetchmanager.h"
59 #include "fetch/fetcherinitializer.h"
60 #include "cite/actionmanager.h"
61 #include "config/tellico_config.h"
62 #include "core/netaccess.h"
63 #include "dbusinterface.h"
64 #include "models/models.h"
65 #include "models/entryiconmodel.h"
66 #include "models/entryselectionmodel.h"
67 #include "newstuff/manager.h"
68 #include "gui/drophandler.h"
69 #include "gui/stringmapdialog.h"
70 #include "gui/lineedit.h"
71 #include "gui/statusbar.h"
72 #include "gui/tabwidget.h"
73 #include "gui/dockwidget.h"
74 #include "utils/cursorsaver.h"
75 #include "utils/guiproxy.h"
76 #include "tellico_debug.h"
77 
78 #include <KComboBox>
79 #include <KToolBar>
80 #include <KLocalizedString>
81 #include <KConfig>
82 #include <KStandardAction>
83 #include <KWindowSystem>
84 #include <KWindowConfig>
85 #include <KMessageBox>
86 #include <KTipDialog>
87 #include <KRecentDocument>
88 #include <KRecentDirs>
89 #include <KEditToolBar>
90 #include <KShortcutsDialog>
91 #include <KRecentFilesAction>
92 #include <KToggleAction>
93 #include <KActionCollection>
94 #include <KActionMenu>
95 #include <KFileWidget>
96 #include <KDualAction>
97 #include <KXMLGUIFactory>
98 #include <KAboutData>
99 #include <kwidgetsaddons_version.h>
100 
101 #include <QApplication>
102 #include <QUndoStack>
103 #include <QAction>
104 #include <QSignalMapper>
105 #include <QTimer>
106 #include <QMetaObject> // needed for copy, cut, paste slots
107 #include <QMimeDatabase>
108 #include <QMimeType>
109 #include <QMenuBar>
110 #include <QFileDialog>
111 #include <QMetaMethod>
112 
113 namespace {
114   static const int MAX_IMAGES_WARN_PERFORMANCE = 200;
115 
mimeIcon(const char * s)116 QIcon mimeIcon(const char* s) {
117   QMimeDatabase db;
118   QMimeType ptr = db.mimeTypeForName(QLatin1String(s));
119   if(!ptr.isValid()) {
120     myDebug() << "*** no icon for" << s;
121   }
122   return ptr.isValid() ? QIcon::fromTheme(ptr.iconName()) : QIcon();
123 }
124 
mimeIcon(const char * s1,const char * s2)125 QIcon mimeIcon(const char* s1, const char* s2) {
126   QMimeDatabase db;
127   QMimeType ptr = db.mimeTypeForName(QLatin1String(s1));
128   if(!ptr.isValid()) {
129     ptr = db.mimeTypeForName(QLatin1String(s2));
130     if(!ptr.isValid()) {
131       myDebug() << "*** no icon for" << s1 << "or" << s2;
132     }
133   }
134   return ptr.isValid() ? QIcon::fromTheme(ptr.iconName()) : QIcon();
135 }
136 
137 }
138 
139 using namespace Tellico;
140 using Tellico::MainWindow;
141 
MainWindow(QWidget * parent_)142 MainWindow::MainWindow(QWidget* parent_/*=0*/) : KXmlGuiWindow(parent_),
143     m_updateAll(nullptr),
144     m_statusBar(nullptr),
145     m_editDialog(nullptr),
146     m_groupView(nullptr),
147     m_filterView(nullptr),
148     m_loanView(nullptr),
149     m_configDlg(nullptr),
150     m_filterDlg(nullptr),
151     m_collFieldsDlg(nullptr),
152     m_stringMacroDlg(nullptr),
153     m_bibtexKeyDlg(nullptr),
154     m_fetchDlg(nullptr),
155     m_reportDlg(nullptr),
156     m_queuedFilters(0),
157     m_initialized(false),
158     m_newDocument(true),
159     m_dontQueueFilter(false),
160     m_savingImageLocationChange(false) {
161 
162   Controller::init(this); // the only time this is ever called!
163   // has to be after controller init
164   Kernel::init(this); // the only time this is ever called!
165   GUI::Proxy::setMainWidget(this);
166 
167   setWindowIcon(QIcon::fromTheme(QStringLiteral("tellico"),
168                                  QIcon(QLatin1String(":/icons/tellico"))));
169 
170   // initialize the status bar and progress bar
171   initStatusBar();
172 
173   // initialize all the collection types
174   // which must be done before the document is created
175   CollectionInitializer initCollections;
176   // register all the fetcher types
177   Fetch::FetcherInitializer initFetchers;
178 
179   // create a document, which also creates an empty book collection
180   // must be done before the different widgets are created
181   initDocument();
182 
183   // set up all the actions, some connect to the document, so this must be after initDocument()
184   initActions();
185 
186   // create the different widgets in the view, some widgets connect to actions, so must be after initActions()
187   initView();
188 
189   // The edit dialog is not created until after the main window is initialized, so it can be a child.
190   // So don't make any connections, don't read options for it until initFileOpen
191   readOptions();
192 
193   setAcceptDrops(true);
194   DropHandler* drophandler = new DropHandler(this);
195   installEventFilter(drophandler);
196 
197   new ApplicationInterface(this);
198   new CollectionInterface(this);
199 
200   MARK_LINE;
201   QTimer::singleShot(0, this, &MainWindow::slotInit);
202 }
203 
~MainWindow()204 MainWindow::~MainWindow() {
205   qDeleteAll(m_fetchActions);
206   m_fetchActions.clear();
207   // when closing the mainwindow, immediately after running Tellico, often there was a long pause
208   // before the application eventually quit, something related to polling on eventfd, I don't
209   // know what. So when closing the window, make sure to immediately quit the application
210   QTimer::singleShot(0, qApp, &QCoreApplication::quit);
211 }
212 
slotInit()213 void MainWindow::slotInit() {
214   // if the edit dialog exists, we know we've already called this function
215   if(m_editDialog) {
216     return;
217   }
218   MARK;
219 
220   m_editDialog = new EntryEditDialog(this);
221   Controller::self()->addObserver(m_editDialog);
222 
223   m_toggleEntryEditor->setChecked(Config::showEditWidget());
224   slotToggleEntryEditor();
225   m_lockLayout->setActive(Config::lockLayout());
226 
227   initConnections();
228   connect(ImageFactory::self(), &ImageFactory::imageLocationMismatch,
229           this, &MainWindow::slotImageLocationMismatch);
230   // Init DBUS for new stuff manager
231   NewStuff::Manager::self();
232 }
233 
initStatusBar()234 void MainWindow::initStatusBar() {
235   MARK;
236   m_statusBar = new Tellico::StatusBar(this);
237   setStatusBar(m_statusBar);
238 }
239 
initActions()240 void MainWindow::initActions() {
241   MARK;
242   /*************************************************
243    * File->New menu
244    *************************************************/
245   QSignalMapper* collectionMapper = new QSignalMapper(this);
246 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
247   void (QSignalMapper::* mappedInt)(int) = &QSignalMapper::mapped;
248   connect(collectionMapper, mappedInt, this, &MainWindow::slotFileNew);
249 #else
250   connect(collectionMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotFileNew);
251 #endif
252 
253   KActionMenu* fileNewMenu = new KActionMenu(i18n("New Collection"), this);
254   fileNewMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
255   fileNewMenu->setToolTip(i18n("Create a new collection"));
256 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
257   fileNewMenu->setPopupMode(QToolButton::InstantPopup);
258 #else
259   fileNewMenu->setDelayed(false);
260 #endif
261   actionCollection()->addAction(QStringLiteral("file_new_collection"), fileNewMenu);
262 
263   QAction* action;
264 
265   void (QSignalMapper::* mapVoid)() = &QSignalMapper::map;
266 #define COLL_ACTION(TYPE, NAME, TEXT, TIP, ICON) \
267   action = actionCollection()->addAction(QStringLiteral(NAME), collectionMapper, mapVoid); \
268   action->setText(TEXT); \
269   action->setToolTip(TIP); \
270   action->setIcon(QIcon(QStringLiteral(":/icons/" ICON))); \
271   fileNewMenu->addAction(action); \
272   collectionMapper->setMapping(action, Data::Collection::TYPE);
273 
274   COLL_ACTION(Book, "new_book_collection", i18n("New &Book Collection"),
275               i18n("Create a new book collection"), "book");
276 
277   COLL_ACTION(Bibtex, "new_bibtex_collection", i18n("New B&ibliography"),
278               i18n("Create a new bibtex bibliography"), "bibtex");
279 
280   COLL_ACTION(ComicBook, "new_comic_book_collection", i18n("New &Comic Book Collection"),
281               i18n("Create a new comic book collection"), "comic");
282 
283   COLL_ACTION(Video, "new_video_collection", i18n("New &Video Collection"),
284               i18n("Create a new video collection"), "video");
285 
286   COLL_ACTION(Album, "new_music_collection", i18n("New &Music Collection"),
287               i18n("Create a new music collection"), "album");
288 
289   COLL_ACTION(Coin, "new_coin_collection", i18n("New C&oin Collection"),
290               i18n("Create a new coin collection"), "coin");
291 
292   COLL_ACTION(Stamp, "new_stamp_collection", i18n("New &Stamp Collection"),
293               i18n("Create a new stamp collection"), "stamp");
294 
295   COLL_ACTION(Card, "new_card_collection", i18n("New C&ard Collection"),
296               i18n("Create a new trading card collection"), "card");
297 
298   COLL_ACTION(Wine, "new_wine_collection", i18n("New &Wine Collection"),
299               i18n("Create a new wine collection"), "wine");
300 
301   COLL_ACTION(Game, "new_game_collection", i18n("New &Game Collection"),
302               i18n("Create a new game collection"), "game");
303 
304   COLL_ACTION(BoardGame, "new_boardgame_collection", i18n("New Boa&rd Game Collection"),
305               i18n("Create a new board game collection"), "boardgame");
306 
307   COLL_ACTION(File, "new_file_catalog", i18n("New &File Catalog"),
308               i18n("Create a new file catalog"), "file");
309 
310   action = actionCollection()->addAction(QStringLiteral("new_custom_collection"), collectionMapper, mapVoid);
311   action->setText(i18n("New C&ustom Collection"));
312   action->setToolTip(i18n("Create a new custom collection"));
313   action->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
314   fileNewMenu->addAction(action);
315   collectionMapper->setMapping(action, Data::Collection::Base);
316 
317 #undef COLL_ACTION
318 
319   /*************************************************
320    * File menu
321    *************************************************/
322   action = KStandardAction::open(this, SLOT(slotFileOpen()), actionCollection());
323   action->setToolTip(i18n("Open an existing document"));
324   m_fileOpenRecent = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(const QUrl&)), actionCollection());
325   m_fileOpenRecent->setToolTip(i18n("Open a recently used file"));
326   m_fileSave = KStandardAction::save(this, SLOT(slotFileSave()), actionCollection());
327   m_fileSave->setToolTip(i18n("Save the document"));
328   action = KStandardAction::saveAs(this, SLOT(slotFileSaveAs()), actionCollection());
329   action->setToolTip(i18n("Save the document as a different file..."));
330   action = KStandardAction::print(this, SLOT(slotFilePrint()), actionCollection());
331   action->setToolTip(i18n("Print the contents of the document..."));
332 #ifdef USE_KHTML
333   {
334     KHTMLPart w;
335     // KHTMLPart printing was broken in KDE until KHTML 5.16
336     const QString version =  w.componentData().version();
337     const uint major = version.section(QLatin1Char('.'), 0, 0).toUInt();
338     const uint minor = version.section(QLatin1Char('.'), 1, 1).toUInt();
339     if(major == 5 && minor < 16) {
340       myWarning() << "Printing is broken for KDE Frameworks < 5.16. Please upgrade";
341       action->setEnabled(false);
342     }
343   }
344 #else
345   // print preview is only available with QWebEngine
346   action = KStandardAction::printPreview(this, SLOT(slotFilePrintPreview()), actionCollection());
347   action->setToolTip(i18n("Preview the contents of the document..."));
348 #endif
349 
350   action = KStandardAction::quit(this, SLOT(slotFileQuit()), actionCollection());
351   action->setToolTip(i18n("Quit the application"));
352 
353 /**************** Import Menu ***************************/
354 
355   QSignalMapper* importMapper = new QSignalMapper(this);
356 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
357   connect(importMapper, mappedInt, this, &MainWindow::slotFileImport);
358 #else
359   connect(importMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotFileImport);
360 #endif
361 
362   KActionMenu* importMenu = new KActionMenu(i18n("&Import"), this);
363   importMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
364   importMenu->setToolTip(i18n("Import the collection data from other formats"));
365 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
366   importMenu->setPopupMode(QToolButton::InstantPopup);
367 #else
368   importMenu->setDelayed(false);
369 #endif
370   actionCollection()->addAction(QStringLiteral("file_import"), importMenu);
371 
372 #define IMPORT_ACTION(TYPE, NAME, TEXT, TIP, ICON) \
373   action = actionCollection()->addAction(QStringLiteral(NAME), importMapper, mapVoid); \
374   action->setText(TEXT); \
375   action->setToolTip(TIP); \
376   action->setIcon(ICON); \
377   importMenu->addAction(action); \
378   importMapper->setMapping(action, TYPE);
379 
380   IMPORT_ACTION(Import::TellicoXML, "file_import_tellico", i18n("Import Tellico Data..."),
381                 i18n("Import another Tellico data file"),
382                 QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico"))));
383 
384   IMPORT_ACTION(Import::CSV, "file_import_csv", i18n("Import CSV Data..."),
385                 i18n("Import a CSV file"), mimeIcon("text/csv", "text/x-csv"));
386 
387   IMPORT_ACTION(Import::MODS, "file_import_mods", i18n("Import MODS Data..."),
388                 i18n("Import a MODS data file"), mimeIcon("text/xml"));
389 
390   IMPORT_ACTION(Import::Alexandria, "file_import_alexandria", i18n("Import Alexandria Data..."),
391                 i18n("Import data from the Alexandria book collection manager"),
392                 QIcon::fromTheme(QStringLiteral("alexandria"), QIcon(QLatin1String(":/icons/alexandria"))));
393 
394   IMPORT_ACTION(Import::Delicious, "file_import_delicious", i18n("Import Delicious Library Data..."),
395                 i18n("Import data from Delicious Library"),
396                 QIcon::fromTheme(QStringLiteral("deliciouslibrary"), QIcon(QLatin1String(":/icons/deliciouslibrary"))));
397 
398   IMPORT_ACTION(Import::Collectorz, "file_import_collectorz", i18n("Import Collectorz Data..."),
399                 i18n("Import data from Collectorz"),
400                 QIcon::fromTheme(QStringLiteral("collectorz"), QIcon(QLatin1String(":/icons/collectorz"))));
401 
402   IMPORT_ACTION(Import::Referencer, "file_import_referencer", i18n("Import Referencer Data..."),
403                 i18n("Import data from Referencer"),
404                 QIcon::fromTheme(QStringLiteral("referencer"), QIcon(QLatin1String(":/icons/referencer"))));
405 
406   IMPORT_ACTION(Import::Bibtex, "file_import_bibtex", i18n("Import Bibtex Data..."),
407                 i18n("Import a bibtex bibliography file"), mimeIcon("text/x-bibtex"));
408 #ifndef ENABLE_BTPARSE
409   action->setEnabled(false);
410 #endif
411 
412   IMPORT_ACTION(Import::Bibtexml, "file_import_bibtexml", i18n("Import Bibtexml Data..."),
413                 i18n("Import a Bibtexml bibliography file"), mimeIcon("text/xml"));
414 
415   IMPORT_ACTION(Import::RIS, "file_import_ris", i18n("Import RIS Data..."),
416                 i18n("Import an RIS reference file"), QIcon::fromTheme(QStringLiteral(":/icons/cite")));
417 
418   IMPORT_ACTION(Import::Goodreads, "file_import_goodreads", i18n("Import Goodreads Collection..."),
419                 i18n("Import a collection from Goodreads.com"), QIcon::fromTheme(QStringLiteral(":/icons/goodreads")));
420 
421   IMPORT_ACTION(Import::LibraryThing, "file_import_librarything", i18n("Import LibraryThing Collection..."),
422                 i18n("Import a collection from LibraryThing.com"), QIcon::fromTheme(QStringLiteral(":/icons/librarything")));
423 
424   IMPORT_ACTION(Import::PDF, "file_import_pdf", i18n("Import PDF File..."),
425                 i18n("Import a PDF file"), mimeIcon("application/pdf"));
426 
427   IMPORT_ACTION(Import::AudioFile, "file_import_audiofile", i18n("Import Audio File Metadata..."),
428                 i18n("Import meta-data from audio files"), mimeIcon("audio/mp3", "audio/x-mp3"));
429 #ifndef HAVE_TAGLIB
430   action->setEnabled(false);
431 #endif
432 
433   IMPORT_ACTION(Import::FreeDB, "file_import_freedb", i18n("Import Audio CD Data..."),
434                 i18n("Import audio CD information"), mimeIcon("media/audiocd", "application/x-cda"));
435 #if !defined (HAVE_KCDDB) && !defined (HAVE_KF5KCDDB)
436   action->setEnabled(false);
437 #endif
438 
439   IMPORT_ACTION(Import::GCstar, "file_import_gcstar", i18n("Import GCstar Data..."),
440                 i18n("Import a GCstar data file"),
441                 QIcon::fromTheme(QStringLiteral("gcstar"), QIcon(QLatin1String(":/icons/gcstar"))));
442 
443   IMPORT_ACTION(Import::Griffith, "file_import_griffith", i18n("Import Griffith Data..."),
444                 i18n("Import a Griffith database"),
445                 QIcon::fromTheme(QStringLiteral("griffith"), QIcon(QLatin1String(":/icons/griffith"))));
446 
447   IMPORT_ACTION(Import::AMC, "file_import_amc", i18n("Import Ant Movie Catalog Data..."),
448                 i18n("Import an Ant Movie Catalog data file"),
449                 QIcon::fromTheme(QStringLiteral("amc"), QIcon(QLatin1String(":/icons/amc"))));
450 
451   IMPORT_ACTION(Import::BoardGameGeek, "file_import_boardgamegeek", i18n("Import BoardGameGeek Collection..."),
452                 i18n("Import a collection from BoardGameGeek.com"), QIcon(QLatin1String(":/icons/boardgamegeek")));
453 
454   IMPORT_ACTION(Import::VinoXML, "file_import_vinoxml", i18n("Import VinoXML..."),
455                 i18n("Import VinoXML data"), QIcon(QLatin1String(":/icons/vinoxml")));
456 
457   IMPORT_ACTION(Import::FileListing, "file_import_filelisting", i18n("Import File Listing..."),
458                 i18n("Import information about files in a folder"), mimeIcon("inode/directory"));
459 
460   IMPORT_ACTION(Import::XSLT, "file_import_xslt", i18n("Import XSL Transform..."),
461                 i18n("Import using an XSL Transform"), mimeIcon("application/xslt+xml", "text/x-xslt"));
462 
463 #undef IMPORT_ACTION
464 
465 /**************** Export Menu ***************************/
466 
467   QSignalMapper* exportMapper = new QSignalMapper(this);
468 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
469   connect(exportMapper, mappedInt, this, &MainWindow::slotFileExport);
470 #else
471   connect(exportMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotFileExport);
472 #endif
473 
474   KActionMenu* exportMenu = new KActionMenu(i18n("&Export"), this);
475   exportMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
476   exportMenu->setToolTip(i18n("Export the collection data to other formats"));
477 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
478   exportMenu->setPopupMode(QToolButton::InstantPopup);
479 #else
480   exportMenu->setDelayed(false);
481 #endif
482   actionCollection()->addAction(QStringLiteral("file_export"), exportMenu);
483 
484 #define EXPORT_ACTION(TYPE, NAME, TEXT, TIP, ICON) \
485   action = actionCollection()->addAction(QStringLiteral(NAME), exportMapper, mapVoid); \
486   action->setText(TEXT); \
487   action->setToolTip(TIP); \
488   action->setIcon(ICON); \
489   exportMenu->addAction(action); \
490   exportMapper->setMapping(action, TYPE);
491 
492   EXPORT_ACTION(Export::TellicoXML, "file_export_xml", i18n("Export to XML..."),
493                 i18n("Export to a Tellico XML file"),
494                 QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QStringLiteral(":/icons/tellico"))));
495 
496   EXPORT_ACTION(Export::TellicoZip, "file_export_zip", i18n("Export to Zip..."),
497                 i18n("Export to a Tellico Zip file"),
498                 QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QStringLiteral(":/icons/tellico"))));
499 
500   EXPORT_ACTION(Export::HTML, "file_export_html", i18n("Export to HTML..."),
501                 i18n("Export to an HTML file"), mimeIcon("text/html"));
502 
503   EXPORT_ACTION(Export::CSV, "file_export_csv", i18n("Export to CSV..."),
504                 i18n("Export to a comma-separated values file"), mimeIcon("text/csv", "text/x-csv"));
505 
506   EXPORT_ACTION(Export::Alexandria, "file_export_alexandria", i18n("Export to Alexandria..."),
507                 i18n("Export to an Alexandria library"),
508                 QIcon::fromTheme(QStringLiteral("alexandria"), QIcon(QStringLiteral(":/icons/alexandria"))));
509 
510   EXPORT_ACTION(Export::Bibtex, "file_export_bibtex", i18n("Export to Bibtex..."),
511                 i18n("Export to a bibtex file"), mimeIcon("text/x-bibtex"));
512 
513   EXPORT_ACTION(Export::Bibtexml, "file_export_bibtexml", i18n("Export to Bibtexml..."),
514                 i18n("Export to a Bibtexml file"), mimeIcon("text/xml"));
515 
516   EXPORT_ACTION(Export::ONIX, "file_export_onix", i18n("Export to ONIX..."),
517                 i18n("Export to an ONIX file"), mimeIcon("text/xml"));
518 
519   EXPORT_ACTION(Export::GCstar, "file_export_gcstar", i18n("Export to GCstar..."),
520                 i18n("Export to a GCstar data file"),
521                 QIcon::fromTheme(QStringLiteral("gcstar"), QIcon(QStringLiteral(":/icons/gcstar"))));
522 
523   EXPORT_ACTION(Export::XSLT, "file_export_xslt", i18n("Export XSL Transform..."),
524                 i18n("Export using an XSL Transform"), mimeIcon("application/xslt+xml", "text/x-xslt"));
525 
526 #undef EXPORT_ACTION
527 
528   /*************************************************
529    * Edit menu
530    *************************************************/
531   KStandardAction::undo(Kernel::self()->commandHistory(), SLOT(undo()), actionCollection());
532   KStandardAction::redo(Kernel::self()->commandHistory(), SLOT(undo()), actionCollection());
533 
534   action = KStandardAction::cut(this, SLOT(slotEditCut()), actionCollection());
535   action->setToolTip(i18n("Cut the selected text and puts it in the clipboard"));
536   action = KStandardAction::copy(this, SLOT(slotEditCopy()), actionCollection());
537   action->setToolTip(i18n("Copy the selected text to the clipboard"));
538   action = KStandardAction::paste(this, SLOT(slotEditPaste()), actionCollection());
539   action->setToolTip(i18n("Paste the clipboard contents"));
540   action = KStandardAction::selectAll(this, SLOT(slotEditSelectAll()), actionCollection());
541   action->setToolTip(i18n("Select all the entries in the collection"));
542   action = KStandardAction::deselect(this, SLOT(slotEditDeselect()), actionCollection());
543   action->setToolTip(i18n("Deselect all the entries in the collection"));
544 
545   action = actionCollection()->addAction(QStringLiteral("filter_dialog"), this, SLOT(slotShowFilterDialog()));
546   action->setText(i18n("Advanced &Filter..."));
547   action->setIconText(i18n("Filter"));
548   action->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
549   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_J);
550   action->setToolTip(i18n("Filter the collection"));
551 
552   /*************************************************
553    * Collection menu
554    *************************************************/
555   m_newEntry = actionCollection()->addAction(QStringLiteral("coll_new_entry"),
556                                              this, SLOT(slotNewEntry()));
557   m_newEntry->setText(i18n("&New Entry..."));
558   m_newEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
559   m_newEntry->setIconText(i18n("New Entry"));
560   actionCollection()->setDefaultShortcut(m_newEntry, Qt::CTRL + Qt::Key_N);
561   m_newEntry->setToolTip(i18n("Create a new entry"));
562 
563   KActionMenu* addEntryMenu = new KActionMenu(i18n("Add Entry"), this);
564   addEntryMenu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
565 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
566   addEntryMenu->setPopupMode(QToolButton::InstantPopup);
567 #else
568   addEntryMenu->setDelayed(false);
569 #endif
570   actionCollection()->addAction(QStringLiteral("coll_add_entry"), addEntryMenu);
571 
572   action = actionCollection()->addAction(QStringLiteral("edit_search_internet"), this, SLOT(slotShowFetchDialog()));
573   action->setText(i18n("Internet Search..."));
574   action->setIconText(i18n("Internet Search"));
575   action->setIcon(QIcon::fromTheme(QStringLiteral("tools-wizard")));
576   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_I);
577   action->setToolTip(i18n("Search the internet..."));
578 
579   addEntryMenu->addAction(m_newEntry);
580   addEntryMenu->addAction(actionCollection()->action(QStringLiteral("edit_search_internet")));
581 
582   m_editEntry = actionCollection()->addAction(QStringLiteral("coll_edit_entry"),
583                                               this, SLOT(slotShowEntryEditor()));
584   m_editEntry->setText(i18n("&Edit Entry..."));
585   m_editEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
586   actionCollection()->setDefaultShortcut(m_editEntry, Qt::CTRL + Qt::Key_E);
587   m_editEntry->setToolTip(i18n("Edit the selected entries"));
588 
589   m_copyEntry = actionCollection()->addAction(QStringLiteral("coll_copy_entry"),
590                                               Controller::self(), SLOT(slotCopySelectedEntries()));
591   m_copyEntry->setText(i18n("D&uplicate Entry"));
592   m_copyEntry->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
593   actionCollection()->setDefaultShortcut(m_copyEntry, Qt::CTRL + Qt::Key_Y);
594   m_copyEntry->setToolTip(i18n("Copy the selected entries"));
595 
596   m_deleteEntry = actionCollection()->addAction(QStringLiteral("coll_delete_entry"),
597                                                 Controller::self(), SLOT(slotDeleteSelectedEntries()));
598   m_deleteEntry->setText(i18n("&Delete Entry"));
599   m_deleteEntry->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
600   actionCollection()->setDefaultShortcut(m_deleteEntry, Qt::CTRL + Qt::Key_D);
601   m_deleteEntry->setToolTip(i18n("Delete the selected entries"));
602 
603   m_mergeEntry = actionCollection()->addAction(QStringLiteral("coll_merge_entry"),
604                                                Controller::self(), SLOT(slotMergeSelectedEntries()));
605   m_mergeEntry->setText(i18n("&Merge Entries"));
606   m_mergeEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
607 //  CTRL+G is ambiguous, pick another
608 //  actionCollection()->setDefaultShortcut(m_mergeEntry, Qt::CTRL + Qt::Key_G);
609   m_mergeEntry->setToolTip(i18n("Merge the selected entries"));
610   m_mergeEntry->setEnabled(false); // gets enabled when more than 1 entry is selected
611 
612   m_checkOutEntry = actionCollection()->addAction(QStringLiteral("coll_checkout"), Controller::self(), SLOT(slotCheckOut()));
613   m_checkOutEntry->setText(i18n("Check-&out..."));
614   m_checkOutEntry->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up-double")));
615   m_checkOutEntry->setToolTip(i18n("Check-out the selected items"));
616 
617   m_checkInEntry = actionCollection()->addAction(QStringLiteral("coll_checkin"), Controller::self(), SLOT(slotCheckIn()));
618   m_checkInEntry->setText(i18n("Check-&in"));
619   m_checkInEntry->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down-double")));
620   m_checkInEntry->setToolTip(i18n("Check-in the selected items"));
621 
622   action = actionCollection()->addAction(QStringLiteral("coll_rename_collection"), this, SLOT(slotRenameCollection()));
623   action->setText(i18n("&Rename Collection..."));
624   action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
625   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_R);
626   action->setToolTip(i18n("Rename the collection"));
627 
628   action = actionCollection()->addAction(QStringLiteral("coll_fields"), this, SLOT(slotShowCollectionFieldsDialog()));
629   action->setText(i18n("Collection &Fields..."));
630   action->setIconText(i18n("Fields"));
631   action->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other")));
632   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_U);
633   action->setToolTip(i18n("Modify the collection fields"));
634 
635   action = actionCollection()->addAction(QStringLiteral("coll_reports"), this, SLOT(slotShowReportDialog()));
636   action->setText(i18n("&Generate Reports..."));
637   action->setIconText(i18n("Reports"));
638   action->setIcon(QIcon::fromTheme(QStringLiteral("text-rdf")));
639   action->setToolTip(i18n("Generate collection reports"));
640 
641   action = actionCollection()->addAction(QStringLiteral("coll_convert_bibliography"), this, SLOT(slotConvertToBibliography()));
642   action->setText(i18n("Convert to &Bibliography"));
643   action->setIcon(QIcon(QLatin1String(":/icons/bibtex")));
644   action->setToolTip(i18n("Convert a book collection to a bibliography"));
645 
646   action = actionCollection()->addAction(QStringLiteral("coll_string_macros"), this, SLOT(slotShowStringMacroDialog()));
647   action->setText(i18n("String &Macros..."));
648   action->setIcon(QIcon::fromTheme(QStringLiteral("view-list-text")));
649   action->setToolTip(i18n("Edit the bibtex string macros"));
650 
651   action = actionCollection()->addAction(QStringLiteral("coll_key_manager"), this, SLOT(slotShowBibtexKeyDialog()));
652   action->setText(i18n("Check for Duplicate Keys..."));
653   action->setIcon(mimeIcon("text/x-bibtex"));
654   action->setToolTip(i18n("Check for duplicate citation keys"));
655 
656   QSignalMapper* citeMapper = new QSignalMapper(this);
657 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
658   connect(citeMapper, mappedInt, this, &MainWindow::slotCiteEntry);
659 #else
660   connect(citeMapper, &QSignalMapper::mappedInt, this, &MainWindow::slotCiteEntry);
661 #endif
662 
663   action = actionCollection()->addAction(QStringLiteral("cite_clipboard"), citeMapper, mapVoid);
664   action->setText(i18n("Copy Bibtex to Cli&pboard"));
665   action->setToolTip(i18n("Copy bibtex citations to the clipboard"));
666   action->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste")));
667   citeMapper->setMapping(action, Cite::CiteClipboard);
668 
669   action = actionCollection()->addAction(QStringLiteral("cite_lyxpipe"), citeMapper, mapVoid);
670   action->setText(i18n("Cite Entry in &LyX"));
671   action->setToolTip(i18n("Cite the selected entries in LyX"));
672   action->setIcon(QIcon::fromTheme(QStringLiteral("lyx"), QIcon(QLatin1String(":/icons/lyx"))));
673   citeMapper->setMapping(action, Cite::CiteLyxpipe);
674 
675   m_updateMapper = new QSignalMapper(this);
676 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
677   void (QSignalMapper::* mappedString)(const QString&) = &QSignalMapper::mapped;
678   connect(m_updateMapper, mappedString,
679           Controller::self(), &Controller::slotUpdateSelectedEntries);
680 #else
681   connect(m_updateMapper, &QSignalMapper::mappedString,
682           Controller::self(), &Controller::slotUpdateSelectedEntries);
683 #endif
684 
685   m_updateEntryMenu = new KActionMenu(i18n("&Update Entry"), this);
686   m_updateEntryMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
687   m_updateEntryMenu->setIconText(i18nc("Update Entry", "Update"));
688 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,77,0)
689   m_updateEntryMenu->setPopupMode(QToolButton::InstantPopup);
690 #else
691   m_updateEntryMenu->setDelayed(false);
692 #endif
693   actionCollection()->addAction(QStringLiteral("coll_update_entry"), m_updateEntryMenu);
694 
695   m_updateAll = actionCollection()->addAction(QStringLiteral("update_entry_all"), m_updateMapper, mapVoid);
696   m_updateAll->setText(i18n("All Sources"));
697   m_updateAll->setToolTip(i18n("Update entry data from all available sources"));
698   m_updateMapper->setMapping(m_updateAll, QStringLiteral("_all"));
699 
700   /*************************************************
701    * Settings menu
702    *************************************************/
703   setStandardToolBarMenuEnabled(true);
704   createStandardStatusBarAction();
705 
706   m_lockLayout = new KDualAction(this);
707   connect(m_lockLayout, &KDualAction::activeChanged, this, &MainWindow::slotToggleLayoutLock);
708   m_lockLayout->setActiveText(i18n("Unlock Layout"));
709   m_lockLayout->setActiveToolTip(i18n("Unlock the window's layout"));
710   m_lockLayout->setActiveIcon(QIcon::fromTheme(QStringLiteral("object-unlocked")));
711   m_lockLayout->setInactiveText(i18n("Lock Layout"));
712   m_lockLayout->setInactiveToolTip(i18n("Lock the window's layout"));
713   m_lockLayout->setInactiveIcon(QIcon::fromTheme(QStringLiteral("object-locked")));
714   actionCollection()->addAction(QStringLiteral("lock_layout"), m_lockLayout);
715 
716   action = actionCollection()->addAction(QStringLiteral("reset_layout"), this, SLOT(slotResetLayout()));
717   action->setText(i18n("Reset Layout"));
718   action->setToolTip(i18n("Reset the window's layout"));
719   action->setIcon(QIcon::fromTheme(QStringLiteral("resetview")));
720 
721   m_toggleEntryEditor = new KToggleAction(i18n("Entry &Editor"), this);
722   connect(m_toggleEntryEditor, &QAction::triggered, this, &MainWindow::slotToggleEntryEditor);
723   m_toggleEntryEditor->setToolTip(i18n("Enable/disable the editor"));
724   actionCollection()->addAction(QStringLiteral("toggle_edit_widget"), m_toggleEntryEditor);
725 
726   KStandardAction::preferences(this, SLOT(slotShowConfigDialog()), actionCollection());
727 
728   /*************************************************
729    * Help menu
730    *************************************************/
731   KStandardAction::tipOfDay(this, SLOT(slotShowTipOfDay()), actionCollection());
732 
733   /*************************************************
734    * Short cuts
735    *************************************************/
736   KStandardAction::fullScreen(this, SLOT(slotToggleFullScreen()), this, actionCollection());
737   KStandardAction::showMenubar(this, SLOT(slotToggleMenuBarVisibility()), actionCollection());
738 
739   /*************************************************
740    * Collection Toolbar
741    *************************************************/
742   action = actionCollection()->addAction(QStringLiteral("change_entry_grouping_accel"), this, SLOT(slotGroupLabelActivated()));
743   action->setText(i18n("Change Grouping"));
744   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_G);
745 
746   m_entryGrouping = new KSelectAction(i18n("&Group Selection"), this);
747   m_entryGrouping->setToolTip(i18n("Change the grouping of the collection"));
748 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,78,0)
749   void (KSelectAction::* triggeredInt)(int) = &KSelectAction::indexTriggered;
750 #else
751   void (KSelectAction::* triggeredInt)(int) = &KSelectAction::triggered;
752 #endif
753   connect(m_entryGrouping, triggeredInt, this, &MainWindow::slotChangeGrouping);
754   actionCollection()->addAction(QStringLiteral("change_entry_grouping"), m_entryGrouping);
755 
756   action = actionCollection()->addAction(QStringLiteral("quick_filter_accel"), this, SLOT(slotFilterLabelActivated()));
757   action->setText(i18n("Filter"));
758   actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F);
759 
760   m_quickFilter = new GUI::LineEdit(this);
761   m_quickFilter->setPlaceholderText(i18n("Filter here...")); // same text as kdepim and amarok
762   m_quickFilter->setClearButtonEnabled(true);
763   // same as Dolphin text edit
764   m_quickFilter->setMinimumWidth(150);
765   m_quickFilter->setMaximumWidth(300);
766   // want to update every time the filter text changes
767   connect(m_quickFilter, &QLineEdit::textChanged,
768           this, &MainWindow::slotQueueFilter);
769   connect(m_quickFilter, &KLineEdit::clearButtonClicked,
770           this, &MainWindow::slotClearFilter);
771   m_quickFilter->installEventFilter(this); // intercept keyEvents
772 
773   QWidgetAction* widgetAction = new QWidgetAction(this);
774   widgetAction->setDefaultWidget(m_quickFilter);
775   widgetAction->setText(i18n("Filter"));
776   widgetAction->setToolTip(i18n("Filter the collection"));
777   widgetAction->setProperty("isShortcutConfigurable", false);
778   actionCollection()->addAction(QStringLiteral("quick_filter"), widgetAction);
779 
780   // final GUI setup is in initView()
781 }
782 
783 #undef mimeIcon
784 
initDocument()785 void MainWindow::initDocument() {
786   MARK;
787   Data::Document* doc = Data::Document::self();
788   Kernel::self()->resetHistory();
789 
790   KConfigGroup config(KSharedConfig::openConfig(), "General Options");
791   doc->setLoadAllImages(config.readEntry("Load All Images", false));
792 
793   // allow status messages from the document
794   connect(doc, &Data::Document::signalStatusMsg,
795           this, &MainWindow::slotStatusMsg);
796 
797   // do stuff that changes when the doc is modified
798   connect(doc, &Data::Document::signalModified,
799           this, &MainWindow::slotEnableModifiedActions);
800 
801   connect(doc, &Data::Document::signalCollectionAdded,
802           Controller::self(), &Controller::slotCollectionAdded);
803   connect(doc, &Data::Document::signalCollectionDeleted,
804           Controller::self(), &Controller::slotCollectionDeleted);
805 
806   connect(Kernel::self()->commandHistory(), &QUndoStack::cleanChanged,
807           doc, &Data::Document::slotSetClean);
808 }
809 
initView()810 void MainWindow::initView() {
811   MARK;
812   // initialize the image factory before the entry models are created
813   ImageFactory::init();
814 
815   m_entryView = new EntryView(this);
816   connect(m_entryView, &EntryView::signalTellicoAction,
817           this, &MainWindow::slotURLAction);
818 
819   // trick to make sure the group views always extend along the entire left or right side
820   // using QMainWindow::setCorner does not seem to work
821   // 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
822   m_dummyWindow = new QMainWindow(this);
823 #ifdef USE_KHTML
824   m_entryView->view()->setWhatsThis(i18n("<qt>The <i>Entry View</i> shows a formatted view of the entry's contents.</qt>"));
825   m_dummyWindow->setCentralWidget(m_entryView->view());
826 #else
827   m_entryView->setWhatsThis(i18n("<qt>The <i>Entry View</i> shows a formatted view of the entry's contents.</qt>"));
828   m_dummyWindow->setCentralWidget(m_entryView);
829 #endif
830   m_dummyWindow->setWindowFlags(Qt::Widget);
831   setCentralWidget(m_dummyWindow);
832 
833   m_collectionViewDock = new GUI::DockWidget(i18n("Collection View"), m_dummyWindow);
834   m_collectionViewDock->setObjectName(QStringLiteral("collection_dock"));
835 
836   m_viewStack = new ViewStack(this);
837 
838   m_detailedView = m_viewStack->listView();
839   Controller::self()->addObserver(m_detailedView);
840   m_detailedView->setWhatsThis(i18n("<qt>The <i>Column View</i> shows the value of multiple fields "
841                                        "for each entry.</qt>"));
842   connect(Data::Document::self(), &Data::Document::signalCollectionImagesLoaded,
843           m_detailedView, &DetailedListView::slotRefreshImages);
844 
845   m_iconView = m_viewStack->iconView();
846   EntryIconModel* iconModel = new EntryIconModel(m_iconView);
847   iconModel->setSourceModel(m_detailedView->model());
848   m_iconView->setModel(iconModel);
849   Controller::self()->addObserver(m_iconView);
850   m_iconView->setWhatsThis(i18n("<qt>The <i>Icon View</i> shows each entry in the collection or group using "
851                                 "an icon, which may be an image in the entry.</qt>"));
852 
853   m_collectionViewDock->setWidget(m_viewStack);
854   m_dummyWindow->addDockWidget(Qt::TopDockWidgetArea, m_collectionViewDock);
855   actionCollection()->addAction(QStringLiteral("toggle_column_widget"), m_collectionViewDock->toggleViewAction());
856 
857   m_groupViewDock = new GUI::DockWidget(i18n("Group View"), this);
858   m_groupViewDock->setObjectName(QStringLiteral("group_dock"));
859   m_groupViewDock->setAllowedAreas(Qt::DockWidgetAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea));
860 
861   m_viewTabs = new GUI::TabWidget(this);
862   m_viewTabs->setTabBarHidden(true);
863   m_viewTabs->setDocumentMode(true);
864   m_groupView = new GroupView(m_viewTabs);
865   Controller::self()->addObserver(m_groupView);
866   m_viewTabs->addTab(m_groupView, QIcon::fromTheme(QStringLiteral("folder")), i18n("Groups"));
867   m_groupView->setWhatsThis(i18n("<qt>The <i>Group View</i> sorts the entries into groupings "
868                                     "based on a selected field.</qt>"));
869   m_groupViewDock->setWidget(m_viewTabs);
870   addDockWidget(Qt::LeftDockWidgetArea, m_groupViewDock);
871   actionCollection()->addAction(QStringLiteral("toggle_group_widget"), m_groupViewDock->toggleViewAction());
872 
873   EntrySelectionModel* proxySelect = new EntrySelectionModel(m_iconView->model(),
874                                                              m_detailedView->selectionModel(),
875                                                              this);
876   m_iconView->setSelectionModel(proxySelect);
877 
878   // setting up GUI now rather than in initActions
879   // initial parameter is default window size
880   setupGUI(QSize(1280,800), Keys | ToolBar);
881   createGUI();
882 }
883 
initConnections()884 void MainWindow::initConnections() {
885   // have to toggle the menu item if the dialog gets closed
886   connect(m_editDialog, &QDialog::finished,
887           this, &MainWindow::slotEditDialogFinished);
888 
889   EntrySelectionModel* proxySelect = static_cast<EntrySelectionModel*>(m_iconView->selectionModel());
890   connect(proxySelect, &EntrySelectionModel::entriesSelected,
891           Controller::self(), &Controller::slotUpdateSelection);
892   connect(proxySelect, &EntrySelectionModel::entriesSelected,
893           m_editDialog, &EntryEditDialog::setContents);
894   connect(proxySelect, &EntrySelectionModel::entriesSelected,
895           m_entryView, &EntryView::showEntries);
896 
897   // let the group view call filters, too
898   connect(m_groupView, &GroupView::signalUpdateFilter,
899           this, &MainWindow::slotUpdateFilter);
900   // use the EntrySelectionModel as a proxy so when entries get selected in the group view
901   // the edit dialog and entry view are updated
902   proxySelect->addSelectionProxy(m_groupView->selectionModel());
903 }
904 
initFileOpen(bool nofile_)905 void MainWindow::initFileOpen(bool nofile_) {
906   MARK;
907   slotInit();
908   // check to see if most recent file should be opened
909   bool happyStart = false;
910   if(!nofile_ && Config::reopenLastFile()) {
911     // Config::lastOpenFile() is the full URL, protocol included
912     QUrl lastFile(Config::lastOpenFile()); // empty string is actually ok, it gets handled
913     if(!lastFile.isEmpty() && lastFile.isValid()) {
914       slotFileOpen(lastFile);
915       happyStart = true;
916     }
917   }
918   if(!happyStart) {
919     // the document is created with an initial book collection, continue with that
920     Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
921 
922     m_fileSave->setEnabled(false);
923     slotEnableOpenedActions();
924     slotEnableModifiedActions(false);
925 
926     slotEntryCount();
927     // tell the entry views and models that there are no images to load
928     m_detailedView->slotRefreshImages();
929   }
930 
931   // show welcome text, even when opening an existing collection
932   const int type = Kernel::self()->collectionType();
933   QString welcomeFile = DataFileRegistry::self()->locate(QStringLiteral("welcome.html"));
934   QString text = FileHandler::readTextFile(QUrl::fromLocalFile(welcomeFile));
935   text.replace(QLatin1String("$FGCOLOR$"), Config::templateTextColor(type).name());
936   text.replace(QLatin1String("$BGCOLOR$"), Config::templateBaseColor(type).name());
937   text.replace(QLatin1String("$COLOR1$"),  Config::templateHighlightedTextColor(type).name());
938   text.replace(QLatin1String("$COLOR2$"),  Config::templateHighlightedBaseColor(type).name());
939   text.replace(QLatin1String("$IMGDIR$"),  QUrl::fromLocalFile(ImageFactory::imageDir()).url());
940   text.replace(QLatin1String("$BANNER$"),
941                 i18n("Welcome to the Tellico Collection Manager"));
942   text.replace(QLatin1String("$WELCOMETEXT$"),
943                 i18n("<h3>Tellico is a tool for managing collections of books, "
944                     "videos, music, and whatever else you want to catalog.</h3>"
945                     "<h3>New entries can be added to your collection by "
946                     "<a href=\"tc:///coll_new_entry\">entering data manually</a> or by "
947                     "<a href=\"tc:///edit_search_internet\">downloading data</a> from "
948                     "various Internet sources.</h3>"));
949   m_entryView->showText(text);
950 
951   m_initialized = true;
952 }
953 
954 // These are general options.
955 // The options that can be changed in the "Configuration..." dialog
956 // are taken care of by the ConfigDialog object.
saveOptions()957 void MainWindow::saveOptions() {
958   KConfigGroup config(KSharedConfig::openConfig(), "Main Window Options");
959   saveMainWindowSettings(config);
960   config.writeEntry(QStringLiteral("Central Dock State"), m_dummyWindow->saveState());
961 
962   Config::setShowEditWidget(m_toggleEntryEditor->isChecked());
963   // check any single dock widget, they all get locked together
964   Config::setLockLayout(m_groupViewDock->isLocked());
965 
966   KConfigGroup filesConfig(KSharedConfig::openConfig(), "Recent Files");
967   m_fileOpenRecent->saveEntries(filesConfig);
968   if(!isNewDocument()) {
969     Config::setLastOpenFile(Data::Document::self()->URL().url());
970   }
971 
972   Config::setViewWidget(m_viewStack->currentWidget());
973 
974   // historical reasons
975   // sorting by count was faked by sorting by phantom second column
976   const int sortColumn = m_groupView->sortRole() == RowCountRole ? 1 : 0;
977   Config::setGroupViewSortColumn(sortColumn); // ok to use SortColumn key, save semantics
978   Config::setGroupViewSortAscending(m_groupView->sortOrder() == Qt::AscendingOrder);
979 
980   if(m_loanView) {
981     const int sortColumn = m_loanView->sortRole() == RowCountRole ? 1 : 0;
982     Config::setLoanViewSortAscending(sortColumn); // ok to use SortColumn key, save semantics
983     Config::setLoanViewSortAscending(m_loanView->sortOrder() == Qt::AscendingOrder);
984   }
985 
986   if(m_filterView) {
987     const int sortColumn = m_filterView->sortRole() == RowCountRole ? 1 : 0;
988     Config::setFilterViewSortAscending(sortColumn); // ok to use SortColumn key, save semantics
989     Config::setFilterViewSortAscending(m_filterView->sortOrder() == Qt::AscendingOrder);
990   }
991 
992   // this is used in the EntryEditDialog constructor, too
993   KConfigGroup editDialogConfig(KSharedConfig::openConfig(), "Edit Dialog Options");
994   KWindowConfig::saveWindowSize(m_editDialog->windowHandle(), editDialogConfig);
995 
996   saveCollectionOptions(Data::Document::self()->collection());
997   Config::self()->save();
998 }
999 
readCollectionOptions(Tellico::Data::CollPtr coll_)1000 void MainWindow::readCollectionOptions(Tellico::Data::CollPtr coll_) {
1001   if(!coll_) {
1002     myDebug() << "Bad, no collection in MainWindow::readCollectionOptions()";
1003     return;
1004   }
1005   const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_));
1006   KConfigGroup group(KSharedConfig::openConfig(), configGroup);
1007 
1008   QString defaultGroup = coll_->defaultGroupField();
1009   QString entryGroup, groupSortField;
1010   if(coll_->type() != Data::Collection::Base) {
1011     entryGroup = group.readEntry("Group By", defaultGroup);
1012     groupSortField = group.readEntry("GroupEntrySortField", QString());
1013   } else {
1014     QUrl url = Kernel::self()->URL();
1015     for(int i = 0; i < Config::maxCustomURLSettings(); ++i) {
1016       QUrl u(group.readEntry(QStringLiteral("URL_%1").arg(i)));
1017       if(url == u) {
1018         entryGroup = group.readEntry(QStringLiteral("Group By_%1").arg(i), defaultGroup);
1019         groupSortField = group.readEntry(QStringLiteral("GroupEntrySortField_%1").arg(i), QString());
1020         break;
1021       }
1022     }
1023     // fall back to old setting
1024     if(entryGroup.isEmpty()) {
1025       entryGroup = group.readEntry("Group By", defaultGroup);
1026     }
1027   }
1028   if(entryGroup.isEmpty() ||
1029      (!coll_->entryGroups().contains(entryGroup) && entryGroup != Data::Collection::s_peopleGroupName)) {
1030     entryGroup = defaultGroup;
1031   }
1032   m_groupView->setGroupField(entryGroup);
1033 
1034   if(!groupSortField.isEmpty()) {
1035     m_groupView->setEntrySortField(groupSortField);
1036   }
1037 
1038   QString entryXSLTFile = Config::templateName(coll_->type());
1039   if(entryXSLTFile.isEmpty()) {
1040     entryXSLTFile = QStringLiteral("Fancy"); // should never happen, but just in case
1041   }
1042   m_entryView->setXSLTFile(entryXSLTFile + QLatin1String(".xsl"));
1043 
1044   // make sure the right combo element is selected
1045   slotUpdateCollectionToolBar(coll_);
1046 }
1047 
saveCollectionOptions(Tellico::Data::CollPtr coll_)1048 void MainWindow::saveCollectionOptions(Tellico::Data::CollPtr coll_) {
1049   // don't save initial collection options, or empty collections
1050   if(!coll_ || coll_->entryCount() == 0 || isNewDocument()) {
1051     return;
1052   }
1053 
1054   int configIndex = -1;
1055   QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_));
1056   KConfigGroup config(KSharedConfig::openConfig(), configGroup);
1057   QString groupName;
1058   const QString groupEntrySort = m_groupView->entrySortField();
1059   if(m_entryGrouping->currentItem() > -1 &&
1060      static_cast<int>(coll_->entryGroups().count()) > m_entryGrouping->currentItem()) {
1061     if(m_entryGrouping->currentText() == (QLatin1Char('<') + i18n("People") + QLatin1Char('>'))) {
1062       groupName = Data::Collection::s_peopleGroupName;
1063     } else {
1064       groupName = Data::Document::self()->collection()->fieldNameByTitle(m_entryGrouping->currentText());
1065     }
1066     if(coll_->type() != Data::Collection::Base) {
1067       config.writeEntry("Group By", groupName);
1068       if(!groupEntrySort.isEmpty()) {
1069         config.writeEntry("GroupEntrySortField", groupEntrySort);
1070       }
1071     }
1072   }
1073 
1074   if(coll_->type() == Data::Collection::Base) {
1075     // all of this is to have custom settings on a per file basis
1076     QUrl url = Kernel::self()->URL();
1077     QList<QUrl> urls = QList<QUrl>() << url;
1078     QStringList groupBys = QStringList() << groupName;
1079     QStringList groupSorts = QStringList() << groupEntrySort;
1080     for(int i = 0; i < Config::maxCustomURLSettings(); ++i) {
1081       QUrl u = config.readEntry(QStringLiteral("URL_%1").arg(i), QUrl());
1082       QString g = config.readEntry(QStringLiteral("Group By_%1").arg(i), QString());
1083       QString gs = config.readEntry(QStringLiteral("GroupEntrySortField_%1").arg(i), QString());
1084       if(!u.isEmpty() && url != u) {
1085         urls.append(u);
1086         groupBys.append(g);
1087         groupSorts.append(gs);
1088       } else if(!u.isEmpty()) {
1089         configIndex = i;
1090       }
1091     }
1092     int limit = qMin(urls.count(), Config::maxCustomURLSettings());
1093     for(int i = 0; i < limit; ++i) {
1094       config.writeEntry(QStringLiteral("URL_%1").arg(i), urls[i].url());
1095       config.writeEntry(QStringLiteral("Group By_%1").arg(i), groupBys[i]);
1096       config.writeEntry(QStringLiteral("GroupEntrySortField_%1").arg(i), groupSorts[i]);
1097     }
1098   }
1099   m_detailedView->saveConfig(coll_, configIndex);
1100 }
1101 
readOptions()1102 void MainWindow::readOptions() {
1103   KConfigGroup mainWindowConfig(KSharedConfig::openConfig(), "Main Window Options");
1104   applyMainWindowSettings(mainWindowConfig);
1105   m_dummyWindow->restoreState(mainWindowConfig.readEntry(QStringLiteral("Central Dock State"), QByteArray()));
1106 
1107   m_viewStack->setCurrentWidget(Config::viewWidget());
1108   m_iconView->setMaxAllowedIconWidth(Config::maxIconSize());
1109 
1110   connect(toolBar(QStringLiteral("collectionToolBar")), &QToolBar::iconSizeChanged, this, &MainWindow::slotUpdateToolbarIcons);
1111 
1112   // initialize the recent file list
1113   KConfigGroup filesConfig(KSharedConfig::openConfig(), "Recent Files");
1114   m_fileOpenRecent->loadEntries(filesConfig);
1115 
1116   // sort by count if column = 1
1117   int sortRole = Config::groupViewSortColumn() == 0 ? static_cast<int>(Qt::DisplayRole) : static_cast<int>(RowCountRole);
1118   Qt::SortOrder sortOrder = Config::groupViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder;
1119   m_groupView->setSorting(sortOrder, sortRole);
1120 
1121   BibtexHandler::s_quoteStyle = Config::useBraces() ? BibtexHandler::BRACES : BibtexHandler::QUOTES;
1122 
1123   // Don't read any options for the edit dialog here, since it's not yet initialized.
1124   // Put them in init()
1125 }
1126 
querySaveModified()1127 bool MainWindow::querySaveModified() {
1128   bool completed = true;
1129 
1130   if(Data::Document::self()->isModified()) {
1131     QString str = i18n("The current file has been modified.\n"
1132                        "Do you want to save it?");
1133     int want_save = KMessageBox::warningYesNoCancel(this, str, i18n("Unsaved Changes"),
1134                                                     KStandardGuiItem::save(), KStandardGuiItem::discard());
1135     switch(want_save) {
1136       case KMessageBox::Yes:
1137         completed = fileSave();
1138         break;
1139 
1140       case KMessageBox::No:
1141         Data::Document::self()->setModified(false);
1142         completed = true;
1143         break;
1144 
1145       case KMessageBox::Cancel:
1146       default:
1147         completed = false;
1148         break;
1149     }
1150   }
1151 
1152   return completed;
1153 }
1154 
queryClose()1155 bool MainWindow::queryClose() {
1156   // in case we're still loading the images, cancel that
1157   Data::Document::self()->cancelImageWriting();
1158   const bool willClose = m_editDialog->queryModified() && querySaveModified();
1159   if(willClose) {
1160     ImageFactory::clean(true);
1161     saveOptions();
1162   }
1163   return willClose;
1164 }
1165 
slotFileNew(int type_)1166 void MainWindow::slotFileNew(int type_) {
1167   slotStatusMsg(i18n("Creating new document..."));
1168 
1169   // close the fields dialog
1170   slotHideCollectionFieldsDialog();
1171 
1172   if(m_editDialog->queryModified() && querySaveModified()) {
1173     // remove filter and loan tabs, they'll get re-added if needed
1174     if(m_filterView) {
1175       m_viewTabs->removeTab(m_viewTabs->indexOf(m_filterView));
1176       Controller::self()->removeObserver(m_filterView);
1177       delete m_filterView;
1178       m_filterView = nullptr;
1179     }
1180     if(m_loanView) {
1181       m_viewTabs->removeTab(m_viewTabs->indexOf(m_loanView));
1182       Controller::self()->removeObserver(m_loanView);
1183       delete m_loanView;
1184       m_loanView = nullptr;
1185     }
1186     m_viewTabs->setTabBarHidden(true);
1187     Data::Document::self()->newDocument(type_);
1188     Kernel::self()->resetHistory();
1189     m_fileOpenRecent->setCurrentItem(-1);
1190     slotEnableOpenedActions();
1191     slotEnableModifiedActions(false);
1192     m_newDocument = true;
1193     ImageFactory::clean(false);
1194   }
1195 
1196   StatusBar::self()->clearStatus();
1197 }
1198 
slotFileOpen()1199 void MainWindow::slotFileOpen() {
1200   slotStatusMsg(i18n("Opening file..."));
1201 
1202   if(m_editDialog->queryModified() && querySaveModified()) {
1203     QString filter = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)");
1204     filter += QLatin1String(";;");
1205     filter += i18n("XML Files") + QLatin1String(" (*.xml)");
1206     filter += QLatin1String(";;");
1207     filter += i18n("All Files") + QLatin1String(" (*)");
1208     // keyword 'open'
1209     QString fileClass;
1210     const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///open")), fileClass);
1211     QUrl url = QFileDialog::getOpenFileUrl(this, i18n("Open File"), startUrl, filter);
1212     if(!url.isEmpty() && url.isValid()) {
1213       slotFileOpen(url);
1214       if(url.isLocalFile()) {
1215         KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1216       }
1217     }
1218   }
1219   StatusBar::self()->clearStatus();
1220 }
1221 
slotFileOpen(const QUrl & url_)1222 void MainWindow::slotFileOpen(const QUrl& url_) {
1223   slotStatusMsg(i18n("Opening file..."));
1224 
1225   // close the fields dialog
1226   slotHideCollectionFieldsDialog();
1227 
1228   // there seems to be a race condition at start between slotInit() and initFileOpen()
1229   // which means the edit dialog might not have been created yet
1230   if((!m_editDialog || m_editDialog->queryModified()) && querySaveModified()) {
1231     if(openURL(url_)) {
1232       m_fileOpenRecent->addUrl(url_);
1233       m_fileOpenRecent->setCurrentItem(-1);
1234     }
1235   }
1236 
1237   StatusBar::self()->clearStatus();
1238 }
1239 
slotFileOpenRecent(const QUrl & url_)1240 void MainWindow::slotFileOpenRecent(const QUrl& url_) {
1241   slotStatusMsg(i18n("Opening file..."));
1242 
1243   // close the fields dialog
1244   slotHideCollectionFieldsDialog();
1245 
1246   if(m_editDialog->queryModified() && querySaveModified()) {
1247     if(!openURL(url_)) {
1248       m_fileOpenRecent->removeUrl(url_);
1249       m_fileOpenRecent->setCurrentItem(-1);
1250     }
1251   } else {
1252     // the QAction shouldn't be checked now
1253     m_fileOpenRecent->setCurrentItem(-1);
1254   }
1255 
1256   StatusBar::self()->clearStatus();
1257 }
1258 
openFile(const QString & file_)1259 void MainWindow::openFile(const QString& file_) {
1260   QUrl url(file_);
1261   if(!url.isEmpty() && url.isValid()) {
1262     slotFileOpen(url);
1263   }
1264 }
1265 
openURL(const QUrl & url_)1266 bool MainWindow::openURL(const QUrl& url_) {
1267   MARK;
1268   // try to open document
1269   GUI::CursorSaver cs(Qt::WaitCursor);
1270 
1271   bool success = Data::Document::self()->openDocument(url_);
1272 
1273   if(success) {
1274     Kernel::self()->resetHistory();
1275     m_quickFilter->clear();
1276     slotEnableOpenedActions();
1277     m_newDocument = false;
1278     slotEnableModifiedActions(Data::Document::self()->isModified()); // doc might add some stuff
1279   } else if(!m_initialized) {
1280     // special case on startup when openURL() is called with a command line argument
1281     // and that URL can't be opened. The window still needs to be initialized
1282     // the doc object is created with an initial book collection, continue with that
1283     Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
1284 
1285     m_fileSave->setEnabled(false);
1286     slotEnableOpenedActions();
1287     slotEnableModifiedActions(false);
1288 
1289     slotEntryCount();
1290   }
1291   // slotFileOpen(URL) gets called when opening files on the command line
1292   // so go ahead and make sure m_initialized is set.
1293   m_initialized = true;
1294 
1295   // remove filter and loan tabs, they'll get re-added if needed
1296   if(m_filterView && m_filterView->isEmpty()) {
1297     m_viewTabs->removeTab(m_viewTabs->indexOf(m_filterView));
1298     Controller::self()->removeObserver(m_filterView);
1299     delete m_filterView;
1300     m_filterView = nullptr;
1301   }
1302   if(m_loanView && m_loanView->isEmpty()) {
1303     m_viewTabs->removeTab(m_viewTabs->indexOf(m_loanView));
1304     Controller::self()->removeObserver(m_loanView);
1305     delete m_loanView;
1306     m_loanView = nullptr;
1307   }
1308   Controller::self()->hideTabs(); // does conditional check
1309 
1310   return success;
1311 }
1312 
slotFileSave()1313 void MainWindow::slotFileSave() {
1314   fileSave();
1315 }
1316 
fileSave()1317 bool MainWindow::fileSave() {
1318   if(!m_editDialog->queryModified()) {
1319     return false;
1320   }
1321   slotStatusMsg(i18n("Saving file..."));
1322 
1323   bool ret = true;
1324   if(isNewDocument()) {
1325     ret = fileSaveAs();
1326   } else {
1327     // special check: if there are more than 200 images AND the "Write Images In File" config key
1328     // is set, then warn user that performance may suffer, and write result
1329     if(Config::imageLocation() == Config::ImagesInFile &&
1330        Config::askWriteImagesInFile() &&
1331        Data::Document::self()->imageCount() > MAX_IMAGES_WARN_PERFORMANCE) {
1332       QString msg = i18n("<qt><p>You are saving a file with many images, which causes Tellico to "
1333                          "slow down significantly. Do you want to save the images separately in "
1334                          "Tellico's data directory to improve performance?</p><p>Your choice can "
1335                          "always be changed in the configuration dialog.</p></qt>");
1336 
1337       KGuiItem yes(i18n("Save Images Separately"));
1338       KGuiItem no(i18n("Save Images in File"));
1339 
1340       int res = KMessageBox::warningYesNo(this, msg, QString() /* caption */, yes, no);
1341       if(res == KMessageBox::No) {
1342         Config::setImageLocation(Config::ImagesInAppDir);
1343       }
1344       Config::setAskWriteImagesInFile(false);
1345     }
1346 
1347     GUI::CursorSaver cs(Qt::WaitCursor);
1348     if(Data::Document::self()->saveDocument(Data::Document::self()->URL())) {
1349       Kernel::self()->resetHistory();
1350       m_newDocument = false;
1351       updateCaption(false);
1352       m_fileSave->setEnabled(false);
1353       // TODO: call a method of the model instead of the view here
1354       m_detailedView->resetEntryStatus();
1355     } else {
1356       ret = false;
1357     }
1358   }
1359 
1360   StatusBar::self()->clearStatus();
1361   return ret;
1362 }
1363 
slotFileSaveAs()1364 void MainWindow::slotFileSaveAs() {
1365   fileSaveAs();
1366 }
1367 
fileSaveAs()1368 bool MainWindow::fileSaveAs() {
1369   if(!m_editDialog->queryModified()) {
1370     return false;
1371   }
1372 
1373   slotStatusMsg(i18n("Saving file with a new filename..."));
1374 
1375   QString filter = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)");
1376   filter += QLatin1String(";;");
1377   filter += i18n("All Files") + QLatin1String(" (*)");
1378 
1379   // keyword 'open'
1380   QString fileClass;
1381   const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///open")), fileClass);
1382   const QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Save As"), startUrl, filter);
1383 
1384   if(url.isEmpty()) {
1385     StatusBar::self()->clearStatus();
1386     return false;
1387   }
1388   if(url.isLocalFile()) {
1389     KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1390   }
1391 
1392   bool ret = true;
1393   if(url.isValid()) {
1394     GUI::CursorSaver cs(Qt::WaitCursor);
1395     m_savingImageLocationChange = true;
1396     // Overwriting an existing file was already confirmed in QFileDialog::getSaveFileUrl()
1397     if(Data::Document::self()->saveDocument(url, true /* force */)) {
1398       Kernel::self()->resetHistory();
1399       KRecentDocument::add(url);
1400       m_fileOpenRecent->addUrl(url);
1401       updateCaption(false);
1402       m_newDocument = false;
1403       m_fileSave->setEnabled(false);
1404       m_detailedView->resetEntryStatus();
1405     } else {
1406       ret = false;
1407     }
1408     m_savingImageLocationChange = false;
1409   }
1410 
1411   StatusBar::self()->clearStatus();
1412   return ret;
1413 }
1414 
slotFilePrint()1415 void MainWindow::slotFilePrint() {
1416   doPrint(Print);
1417 }
1418 
slotFilePrintPreview()1419 void MainWindow::slotFilePrintPreview() {
1420   doPrint(PrintPreview);
1421 }
1422 
doPrint(PrintAction action_)1423 void MainWindow::doPrint(PrintAction action_) {
1424   slotStatusMsg(i18n("Printing..."));
1425 
1426   // If the collection is being filtered, warn the user
1427   if(m_detailedView->filter()) {
1428     QString str = i18n("The collection is currently being filtered to show a limited subset of "
1429                        "the entries. Only the visible entries will be printed. Continue?");
1430     int ret = KMessageBox::warningContinueCancel(this, str, QString(), KStandardGuiItem::print(),
1431                                                  KStandardGuiItem::cancel(), QStringLiteral("WarnPrintVisible"));
1432     if(ret == KMessageBox::Cancel) {
1433       StatusBar::self()->clearStatus();
1434       return;
1435     }
1436   }
1437 
1438   PrintHandler printHandler(this);
1439   printHandler.setEntries(m_detailedView->visibleEntries());
1440   printHandler.setColumns(m_detailedView->visibleColumns());
1441   if(action_ == Print) {
1442     printHandler.print();
1443   } else {
1444     printHandler.printPreview();
1445   }
1446 
1447   StatusBar::self()->clearStatus();
1448 }
1449 
slotFileQuit()1450 void MainWindow::slotFileQuit() {
1451   slotStatusMsg(i18n("Exiting..."));
1452 
1453   close(); // will call queryClose()
1454 
1455   StatusBar::self()->clearStatus();
1456 }
1457 
slotEditCut()1458 void MainWindow::slotEditCut() {
1459   activateEditSlot("cut()");
1460 }
1461 
slotEditCopy()1462 void MainWindow::slotEditCopy() {
1463   activateEditSlot("copy()");
1464 }
1465 
slotEditPaste()1466 void MainWindow::slotEditPaste() {
1467   activateEditSlot("paste()");
1468 }
1469 
activateEditSlot(const char * slot_)1470 void MainWindow::activateEditSlot(const char* slot_) {
1471   // the edit widget is the only one that copies, cuts, and pastes
1472   // the entry view can copy
1473   QWidget* w;
1474   if(m_editDialog->isVisible()) {
1475     w = m_editDialog->focusWidget();
1476   } else {
1477     w = qApp->focusWidget();
1478   }
1479 
1480   while(w && w->isVisible()) {
1481     const QMetaObject* meta = w->metaObject();
1482     const int idx = meta->indexOfSlot(slot_);
1483     if(idx > -1) {
1484 //      myDebug() << "MainWindow invoking" << meta->method(idx).methodSignature();
1485       meta->method(idx).invoke(w, Qt::DirectConnection);
1486       break;
1487     } else {
1488 //      myDebug() << "did not find" << slot_ << "in" << meta->className();
1489       w = qobject_cast<QWidget*>(w->parent());
1490     }
1491   }
1492 }
1493 
slotEditSelectAll()1494 void MainWindow::slotEditSelectAll() {
1495   m_detailedView->selectAllVisible();
1496 }
1497 
slotEditDeselect()1498 void MainWindow::slotEditDeselect() {
1499   Controller::self()->slotUpdateSelection(Data::EntryList());
1500 }
1501 
slotToggleEntryEditor()1502 void MainWindow::slotToggleEntryEditor() {
1503   if(m_toggleEntryEditor->isChecked()) {
1504     m_editDialog->show();
1505   } else {
1506     m_editDialog->hide();
1507   }
1508 }
1509 
slotShowConfigDialog()1510 void MainWindow::slotShowConfigDialog() {
1511   if(!m_configDlg) {
1512     m_configDlg = new ConfigDialog(this);
1513     connect(m_configDlg, &ConfigDialog::signalConfigChanged,
1514             this, &MainWindow::slotHandleConfigChange);
1515     connect(m_configDlg, &QDialog::finished,
1516             this, &MainWindow::slotHideConfigDialog);
1517   } else {
1518     KWindowSystem::activateWindow(m_configDlg->winId());
1519   }
1520   m_configDlg->show();
1521 }
1522 
slotHideConfigDialog()1523 void MainWindow::slotHideConfigDialog() {
1524   if(m_configDlg) {
1525     m_configDlg->hide();
1526     m_configDlg->deleteLater();
1527     m_configDlg = nullptr;
1528   }
1529 }
1530 
slotShowTipOfDay(bool force_)1531 void MainWindow::slotShowTipOfDay(bool force_/*=true*/) {
1532   KTipDialog::showTip(this, QStringLiteral("tellico/tellico.tips"), force_);
1533 }
1534 
slotStatusMsg(const QString & text_)1535 void MainWindow::slotStatusMsg(const QString& text_) {
1536   m_statusBar->setStatus(text_);
1537 }
1538 
slotClearStatus()1539 void MainWindow::slotClearStatus() {
1540   StatusBar::self()->clearStatus();
1541 }
1542 
slotEntryCount()1543 void MainWindow::slotEntryCount() {
1544   Data::CollPtr coll = Data::Document::self()->collection();
1545   if(!coll) {
1546     return;
1547   }
1548 
1549   int count = coll->entryCount();
1550   QString text = i18n("Total entries: %1", count);
1551 
1552   int selectCount = Controller::self()->selectedEntries().count();
1553   int filterCount = m_detailedView->visibleItems();
1554   // if more than one book is selected, add the number of selected books
1555   if(filterCount < count && selectCount > 1) {
1556     text += QLatin1Char(' ');
1557     text += i18n("(%1 filtered; %2 selected)", filterCount, selectCount);
1558   } else if(filterCount < count) {
1559     text += QLatin1Char(' ');
1560     text += i18n("(%1 filtered)", filterCount);
1561   } else if(selectCount > 1) {
1562     text += QLatin1Char(' ');
1563     text += i18n("(%1 selected)", selectCount);
1564   }
1565 
1566   m_statusBar->setCount(text);
1567 }
1568 
slotEnableOpenedActions()1569 void MainWindow::slotEnableOpenedActions() {
1570   slotUpdateToolbarIcons();
1571 
1572   updateCollectionActions();
1573 
1574   // close the filter dialog when a new collection is opened
1575   slotHideFilterDialog();
1576   slotStringMacroDialogFinished();
1577 }
1578 
slotEnableModifiedActions(bool modified_)1579 void MainWindow::slotEnableModifiedActions(bool modified_ /*= true*/) {
1580   updateCaption(modified_);
1581   updateCollectionActions();
1582   m_fileSave->setEnabled(modified_);
1583 }
1584 
slotHandleConfigChange()1585 void MainWindow::slotHandleConfigChange() {
1586   const int imageLocation = Config::imageLocation();
1587   const bool autoCapitalize = Config::autoCapitalization();
1588   const bool autoFormat = Config::autoFormat();
1589   const QStringList articles = Config::articleList();
1590   const QStringList nocaps = Config::noCapitalizationList();
1591   const QStringList suffixes = Config::nameSuffixList();
1592   const QStringList prefixes = Config::surnamePrefixList();
1593 
1594   m_configDlg->saveConfiguration();
1595 
1596   // only modified if there are entries and image location is changed
1597   if(imageLocation != Config::imageLocation() && !Data::Document::self()->isEmpty()) {
1598     slotImageLocationChanged();
1599   }
1600 
1601   if(autoCapitalize != Config::autoCapitalization() ||
1602     autoFormat != Config::autoFormat() ||
1603     articles != Config::articleList() ||
1604     nocaps != Config::noCapitalizationList() ||
1605     suffixes != Config::nameSuffixList() ||
1606     prefixes != Config::surnamePrefixList()) {
1607     // invalidate all groups
1608     Data::Document::self()->collection()->invalidateGroups();
1609     // refreshing the title causes the group view to refresh
1610     Controller::self()->slotRefreshField(Data::Document::self()->collection()->fieldByName(QStringLiteral("title")));
1611   }
1612 
1613   QString entryXSLTFile = Config::templateName(Kernel::self()->collectionType());
1614   m_entryView->setXSLTFile(entryXSLTFile + QLatin1String(".xsl"));
1615 }
1616 
slotUpdateCollectionToolBar(Tellico::Data::CollPtr coll_)1617 void MainWindow::slotUpdateCollectionToolBar(Tellico::Data::CollPtr coll_) {
1618   if(!coll_) {
1619     myWarning() << "no collection pointer!";
1620     return;
1621   }
1622 
1623   QString current = m_groupView->groupBy();
1624   if(current.isEmpty() || !coll_->entryGroups().contains(current)) {
1625     current = coll_->defaultGroupField();
1626   }
1627 
1628   const QStringList groups = coll_->entryGroups();
1629   if(groups.isEmpty()) {
1630     m_entryGrouping->clear();
1631     return;
1632   }
1633 
1634   QMap<QString, QString> groupMap; // use a map so they get sorted
1635   foreach(const QString& groupName, groups) {
1636     // special case for people "pseudo-group"
1637     if(groupName == Data::Collection::s_peopleGroupName) {
1638       groupMap.insert(groupName, QLatin1Char('<') + i18n("People") + QLatin1Char('>'));
1639     } else {
1640       groupMap.insert(groupName, coll_->fieldTitleByName(groupName));
1641     }
1642   }
1643 
1644   const QStringList titles = groupMap.values();
1645   if(titles == m_entryGrouping->items()) {
1646     // no need to update anything
1647     return;
1648   }
1649   const QStringList names = groupMap.keys();
1650   int index = names.indexOf(current);
1651   if(index == -1) {
1652     current = names[0];
1653     index = 0;
1654   }
1655   m_entryGrouping->setItems(titles);
1656   m_entryGrouping->setCurrentItem(index);
1657   // in case the current grouping field get modified to be non-grouping...
1658   m_groupView->setGroupField(current); // don't call slotChangeGrouping() since it adds an undo item
1659 
1660   // TODO::I have no idea how to get the combobox to update its size
1661   // this is the hackiest of hacks, taken from KXmlGuiWindow::saveNewToolbarConfig()
1662   // the window flickers as toolbar resizes, unavoidable?
1663   // crashes if removeClient//addClient is called here, need to do later in event loop
1664   QTimer::singleShot(0, this, &MainWindow::guiFactoryReset);
1665 }
1666 
slotChangeGrouping()1667 void MainWindow::slotChangeGrouping() {
1668   const QString title = m_entryGrouping->currentText();
1669 
1670   QString groupName = Data::Document::self()->collection()->fieldNameByTitle(title);
1671   if(groupName.isEmpty()) {
1672     if(title == (QLatin1Char('<') + i18n("People") + QLatin1Char('>'))) {
1673       groupName = Data::Collection::s_peopleGroupName;
1674     } else {
1675       groupName = Data::Document::self()->collection()->defaultGroupField();
1676     }
1677   }
1678   m_groupView->setGroupField(groupName);
1679   m_viewTabs->setCurrentWidget(m_groupView);
1680 }
1681 
slotShowReportDialog()1682 void MainWindow::slotShowReportDialog() {
1683   if(!m_reportDlg) {
1684     m_reportDlg = new ReportDialog(this);
1685     connect(m_reportDlg, &QDialog::finished,
1686             this, &MainWindow::slotHideReportDialog);
1687   } else {
1688     KWindowSystem::activateWindow(m_reportDlg->winId());
1689   }
1690   m_reportDlg->show();
1691 }
1692 
slotHideReportDialog()1693 void MainWindow::slotHideReportDialog() {
1694   if(m_reportDlg) {
1695     m_reportDlg->hide();
1696     m_reportDlg->deleteLater();
1697     m_reportDlg = nullptr;
1698   }
1699 }
1700 
XSLTError()1701 void MainWindow::XSLTError() {
1702   QString str = i18n("Tellico encountered an error in XSLT processing.") + QLatin1Char('\n');
1703   str += i18n("Please check your installation.");
1704   Kernel::self()->sorry(str);
1705 }
1706 
slotShowFilterDialog()1707 void MainWindow::slotShowFilterDialog() {
1708   if(!m_filterDlg) {
1709     m_filterDlg = new FilterDialog(FilterDialog::CreateFilter, this); // allow saving
1710     m_quickFilter->setEnabled(false);
1711     connect(m_filterDlg, &FilterDialog::signalCollectionModified,
1712             Data::Document::self(), &Data::Document::slotSetModified);
1713     connect(m_filterDlg, &FilterDialog::signalUpdateFilter,
1714             this, &MainWindow::slotUpdateFilter);
1715     connect(m_filterDlg, &QDialog::finished,
1716             this, &MainWindow::slotHideFilterDialog);
1717   } else {
1718     KWindowSystem::activateWindow(m_filterDlg->winId());
1719   }
1720   m_filterDlg->setFilter(m_detailedView->filter());
1721   m_filterDlg->show();
1722 }
1723 
slotHideFilterDialog()1724 void MainWindow::slotHideFilterDialog() {
1725 //  m_quickFilter->blockSignals(false);
1726   m_quickFilter->setEnabled(true);
1727   if(m_filterDlg) {
1728     m_filterDlg->hide();
1729     m_filterDlg->deleteLater();
1730     m_filterDlg = nullptr;
1731   }
1732 }
1733 
slotQueueFilter()1734 void MainWindow::slotQueueFilter() {
1735   if(m_dontQueueFilter) {
1736     return;
1737   }
1738   m_queuedFilters++;
1739   QTimer::singleShot(200, this, &MainWindow::slotCheckFilterQueue);
1740 }
1741 
slotCheckFilterQueue()1742 void MainWindow::slotCheckFilterQueue() {
1743   m_queuedFilters--;
1744   if(m_queuedFilters > 0) {
1745     return;
1746   }
1747 
1748   setFilter(m_quickFilter->text());
1749 }
1750 
slotUpdateFilter(FilterPtr filter_)1751 void MainWindow::slotUpdateFilter(FilterPtr filter_) {
1752   // Can't just block signals because clear button won't show then
1753   m_dontQueueFilter = true;
1754   m_quickFilter->setText(QStringLiteral(" ")); // To be able to clear custom filter
1755   Controller::self()->slotUpdateFilter(filter_);
1756   m_dontQueueFilter = false;
1757 }
1758 
setFilter(const QString & text_)1759 void MainWindow::setFilter(const QString& text_) {
1760   QString text = text_.trimmed();
1761   FilterPtr filter;
1762   if(!text.isEmpty()) {
1763     filter = new Filter(Filter::MatchAll);
1764     QString fieldName; // empty field name means match on any field
1765     // if the text contains '=' assume it's a field name or title
1766     if(text.indexOf(QLatin1Char('=')) > -1) {
1767       fieldName = text.section(QLatin1Char('='), 0, 0).trimmed();
1768       text = text.section(QLatin1Char('='), 1).trimmed();
1769       // check that the field name might be a title
1770       if(!Data::Document::self()->collection()->hasField(fieldName)) {
1771         fieldName = Data::Document::self()->collection()->fieldNameByTitle(fieldName);
1772       }
1773     }
1774     // if the text contains any non-word characters, assume it's a regexp
1775     // but \W in qt is letter, number, or '_', I want to be a bit less strict
1776     QRegularExpression rx(QLatin1String("[^\\w\\s\\-']"));
1777     if(!rx.match(text).hasMatch()) {
1778       // split by whitespace, and add rules for each word
1779       const QStringList tokens = text.split(QRegularExpression(QLatin1String("\\s")));
1780       foreach(const QString& token, tokens) {
1781         // an empty field string means check every field
1782         filter->append(new FilterRule(fieldName, token, FilterRule::FuncContains));
1783       }
1784     } else {
1785       // if it isn't valid, hold off on applying the filter
1786       QRegularExpression tx(text);
1787       if(!tx.isValid()) {
1788         text = QRegularExpression::escape(text);
1789         tx.setPattern(text);
1790       }
1791       if(!tx.isValid()) {
1792         myDebug() << "invalid regular expression:" << text;
1793         return;
1794       }
1795       filter->append(new FilterRule(fieldName, text, FilterRule::FuncRegExp));
1796     }
1797     // also want to update the line edit in case the filter was set by DBUS
1798     if(m_quickFilter->text() != text_) {
1799       m_quickFilter->setText(text_);
1800     }
1801   }
1802   // only update filter if one exists or did exist
1803   if(filter || m_detailedView->filter()) {
1804     Controller::self()->slotUpdateFilter(filter);
1805   }
1806 }
1807 
slotShowCollectionFieldsDialog()1808 void MainWindow::slotShowCollectionFieldsDialog() {
1809   if(!m_collFieldsDlg) {
1810     m_collFieldsDlg = new CollectionFieldsDialog(Data::Document::self()->collection(), this);
1811     connect(m_collFieldsDlg, &QDialog::finished,
1812             this, &MainWindow::slotHideCollectionFieldsDialog);
1813   } else {
1814     KWindowSystem::activateWindow(m_collFieldsDlg->winId());
1815   }
1816   m_collFieldsDlg->show();
1817 }
1818 
slotHideCollectionFieldsDialog()1819 void MainWindow::slotHideCollectionFieldsDialog() {
1820   if(m_collFieldsDlg) {
1821     m_collFieldsDlg->hide();
1822     m_collFieldsDlg->deleteLater();
1823     m_collFieldsDlg = nullptr;
1824   }
1825 }
1826 
slotFileImport(int format_)1827 void MainWindow::slotFileImport(int format_) {
1828   slotStatusMsg(i18n("Importing data..."));
1829   m_quickFilter->clear();
1830 
1831   Import::Format format = static_cast<Import::Format>(format_);
1832   bool checkURL = true;
1833   QUrl url;
1834   switch(ImportDialog::importTarget(format)) {
1835     case Import::File:
1836       {
1837         QString fileClass;
1838         const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///import")), fileClass);
1839         url = QFileDialog::getOpenFileUrl(this, i18n("Import File"), startUrl, ImportDialog::fileFilter(format));
1840         KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1841       }
1842       break;
1843 
1844     case Import::Dir:
1845       // TODO: allow remote audiofile importing
1846       {
1847         const QString fileClass(QStringLiteral("ImportDir"));
1848         QString dirName = ImportDialog::startDir(format);
1849         if(dirName.isEmpty()) {
1850           dirName = KRecentDirs::dir(fileClass);
1851         }
1852         QString chosenDir = QFileDialog::getExistingDirectory(this, i18n("Import Directory"), dirName);
1853         url = QUrl::fromLocalFile(chosenDir);
1854         KRecentDirs::add(fileClass, chosenDir);
1855       }
1856       break;
1857 
1858     case Import::None:
1859     default:
1860       checkURL = false;
1861       break;
1862   }
1863 
1864   if(checkURL) {
1865     bool ok = !url.isEmpty() && url.isValid() && QFile::exists(url.toLocalFile());
1866     if(!ok) {
1867       StatusBar::self()->clearStatus();
1868       return;
1869     }
1870   }
1871   importFile(format, QList<QUrl>() << url);
1872   StatusBar::self()->clearStatus();
1873 }
1874 
slotFileExport(int format_)1875 void MainWindow::slotFileExport(int format_) {
1876   slotStatusMsg(i18n("Exporting data..."));
1877 
1878   Export::Format format = static_cast<Export::Format>(format_);
1879   ExportDialog dlg(format, Data::Document::self()->collection(), this);
1880 
1881   if(dlg.exec() == QDialog::Rejected) {
1882     StatusBar::self()->clearStatus();
1883     return;
1884   }
1885 
1886   switch(ExportDialog::exportTarget(format)) {
1887     case Export::None:
1888       dlg.exportURL();
1889       break;
1890 
1891     case Export::Dir:
1892       myDebug() << "ExportDir not implemented!";
1893       break;
1894 
1895     case Export::File:
1896     {
1897       QString fileClass;
1898       const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///export")), fileClass);
1899       QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Export As"), startUrl, dlg.fileFilter());
1900       if(url.isEmpty()) {
1901         StatusBar::self()->clearStatus();
1902         return;
1903       }
1904 
1905       if(url.isValid()) {
1906         if(url.isLocalFile()) {
1907           KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
1908         }
1909         GUI::CursorSaver cs(Qt::WaitCursor);
1910         dlg.exportURL(url);
1911       }
1912     }
1913     break;
1914   }
1915 
1916   StatusBar::self()->clearStatus();
1917 }
1918 
slotShowStringMacroDialog()1919 void MainWindow::slotShowStringMacroDialog() {
1920   if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) {
1921     return;
1922   }
1923 
1924   if(!m_stringMacroDlg) {
1925     const Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data());
1926     m_stringMacroDlg = new StringMapDialog(c->macroList(), this, false);
1927     m_stringMacroDlg->setWindowTitle(i18n("String Macros"));
1928     m_stringMacroDlg->setLabels(i18n("Macro"), i18n("String"));
1929     connect(m_stringMacroDlg, &QDialog::finished, this, &MainWindow::slotStringMacroDialogFinished);
1930   } else {
1931     KWindowSystem::activateWindow(m_stringMacroDlg->winId());
1932   }
1933   m_stringMacroDlg->show();
1934 }
1935 
slotStringMacroDialogFinished(int result_)1936 void MainWindow::slotStringMacroDialogFinished(int result_) {
1937   // no point in checking if collection is bibtex, as dialog would never have been created
1938   if(!m_stringMacroDlg) {
1939     return;
1940   }
1941   if(result_ == QDialog::Accepted) {
1942     static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data())->setMacroList(m_stringMacroDlg->stringMap());
1943     Data::Document::self()->setModified(true);
1944   }
1945   m_stringMacroDlg->hide();
1946   m_stringMacroDlg->deleteLater();
1947   m_stringMacroDlg = nullptr;
1948 }
1949 
slotShowBibtexKeyDialog()1950 void MainWindow::slotShowBibtexKeyDialog() {
1951   if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) {
1952     return;
1953   }
1954 
1955   if(!m_bibtexKeyDlg) {
1956     m_bibtexKeyDlg = new BibtexKeyDialog(Data::Document::self()->collection(), this);
1957     connect(m_bibtexKeyDlg, &QDialog::finished, this, &MainWindow::slotHideBibtexKeyDialog);
1958     connect(m_bibtexKeyDlg, &BibtexKeyDialog::signalUpdateFilter,
1959             this, &MainWindow::slotUpdateFilter);
1960   } else {
1961     KWindowSystem::activateWindow(m_bibtexKeyDlg->winId());
1962   }
1963   m_bibtexKeyDlg->show();
1964 }
1965 
slotHideBibtexKeyDialog()1966 void MainWindow::slotHideBibtexKeyDialog() {
1967   if(m_bibtexKeyDlg) {
1968     m_bibtexKeyDlg->deleteLater();
1969     m_bibtexKeyDlg = nullptr;
1970   }
1971 }
1972 
slotNewEntry()1973 void MainWindow::slotNewEntry() {
1974   m_toggleEntryEditor->setChecked(true);
1975   slotToggleEntryEditor();
1976   m_editDialog->slotHandleNew();
1977 }
1978 
slotEditDialogFinished()1979 void MainWindow::slotEditDialogFinished() {
1980   m_toggleEntryEditor->setChecked(false);
1981 }
1982 
slotShowEntryEditor()1983 void MainWindow::slotShowEntryEditor() {
1984   m_toggleEntryEditor->setChecked(true);
1985   m_editDialog->show();
1986 
1987   KWindowSystem::activateWindow(m_editDialog->winId());
1988 }
1989 
slotConvertToBibliography()1990 void MainWindow::slotConvertToBibliography() {
1991   // only book collections can be converted to bibtex
1992   Data::CollPtr coll = Data::Document::self()->collection();
1993   if(!coll || coll->type() != Data::Collection::Book) {
1994     return;
1995   }
1996 
1997   GUI::CursorSaver cs;
1998 
1999   // need to make sure all images are transferred
2000   Data::Document::self()->loadAllImagesNow();
2001 
2002   Data::CollPtr newColl = Data::BibtexCollection::convertBookCollection(coll);
2003   if(newColl) {
2004     m_newDocument = true;
2005     Kernel::self()->replaceCollection(newColl);
2006     m_fileOpenRecent->setCurrentItem(-1);
2007     slotUpdateToolbarIcons();
2008     updateCollectionActions();
2009   } else {
2010     myWarning() << "ERROR: no bibliography created!";
2011   }
2012 }
2013 
slotCiteEntry(int action_)2014 void MainWindow::slotCiteEntry(int action_) {
2015   StatusBar::self()->setStatus(i18n("Creating citations..."));
2016   Cite::ActionManager* man = Cite::ActionManager::self();
2017   man->cite(static_cast<Cite::CiteAction>(action_), Controller::self()->selectedEntries());
2018   if(man->hasError()) {
2019     Kernel::self()->sorry(man->errorString());
2020   }
2021   StatusBar::self()->clearStatus();
2022 }
2023 
slotShowFetchDialog()2024 void MainWindow::slotShowFetchDialog() {
2025   if(!m_fetchDlg) {
2026     m_fetchDlg = new FetchDialog(this);
2027     connect(m_fetchDlg, &QDialog::finished, this, &MainWindow::slotHideFetchDialog);
2028     connect(Controller::self(), &Controller::collectionAdded, m_fetchDlg, &FetchDialog::slotResetCollection);
2029   } else {
2030     KWindowSystem::activateWindow(m_fetchDlg->winId());
2031   }
2032   m_fetchDlg->show();
2033 }
2034 
slotHideFetchDialog()2035 void MainWindow::slotHideFetchDialog() {
2036   if(m_fetchDlg) {
2037     m_fetchDlg->hide();
2038     m_fetchDlg->deleteLater();
2039     m_fetchDlg = nullptr;
2040   }
2041 }
2042 
importFile(Tellico::Import::Format format_,const QUrl & url_,Tellico::Import::Action action_)2043 bool MainWindow::importFile(Tellico::Import::Format format_, const QUrl& url_, Tellico::Import::Action action_) {
2044   // try to open document
2045   GUI::CursorSaver cs(Qt::WaitCursor);
2046 
2047   bool failed = false;
2048   Data::CollPtr coll;
2049   if(!url_.isEmpty() && url_.isValid() && NetAccess::exists(url_, true, this)) {
2050     coll = ImportDialog::importURL(format_, url_);
2051   } else {
2052     Kernel::self()->sorry(i18n(errorLoad, url_.fileName()));
2053     failed = true;
2054   }
2055 
2056   if(!coll && !m_initialized) {
2057     // special case on startup when openURL() is called with a command line argument
2058     // and that URL can't be opened. The window still needs to be initialized
2059     // the doc object is created with an initial book collection, continue with that
2060     Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
2061     m_fileSave->setEnabled(false);
2062     slotEnableOpenedActions();
2063     slotEnableModifiedActions(false);
2064     slotEntryCount();
2065     m_fileOpenRecent->setCurrentItem(-1);
2066     m_initialized = true;
2067     failed = true;
2068   } else if(coll) {
2069     // this is rather dumb, but I'm too lazy to find the bug
2070     // if the document isn't initialized, then Tellico crashes
2071     // since Document::replaceCollection() ends up calling lots of stuff that isn't initialized
2072     if(!m_initialized) {
2073       Controller::self()->slotCollectionAdded(Data::Document::self()->collection());
2074       m_initialized = true;
2075     }
2076     failed = !importCollection(coll, action_);
2077   }
2078 
2079   StatusBar::self()->clearStatus();
2080   return !failed; // return true means success
2081 }
2082 
exportCollection(Tellico::Export::Format format_,const QUrl & url_,bool filtered_)2083 bool MainWindow::exportCollection(Tellico::Export::Format format_, const QUrl& url_, bool filtered_) {
2084   if(!url_.isValid()) {
2085     myDebug() << "invalid URL:" << url_;
2086     return false;
2087   }
2088 
2089   GUI::CursorSaver cs;
2090   const Data::CollPtr coll = Data::Document::self()->collection();
2091   if(!coll) {
2092     return false;
2093   }
2094 
2095   // only bibliographies can export to bibtex or bibtexml
2096   const bool isBibtex = (coll->type() == Data::Collection::Bibtex);
2097   if(!isBibtex && (format_ == Export::Bibtex || format_ == Export::Bibtexml)) {
2098     return false;
2099   }
2100   // only books and bibliographies can export to alexandria
2101   const bool isBook = (coll->type() == Data::Collection::Book);
2102   if(!isBibtex && !isBook && format_ == Export::Alexandria) {
2103     return false;
2104   }
2105 
2106   return ExportDialog::exportCollection(coll, filtered_ ? Controller::self()->visibleEntries() : coll->entries(),
2107                                         format_, url_);
2108 }
2109 
showEntry(Data::ID id)2110 bool MainWindow::showEntry(Data::ID id) {
2111   Data::EntryPtr entry = Data::Document::self()->collection()->entryById(id);
2112   if(entry) {
2113     m_entryView->showEntry(entry);
2114   }
2115   return entry;
2116 }
2117 
addFilterView()2118 void MainWindow::addFilterView() {
2119   if(m_filterView) {
2120     return;
2121   }
2122 
2123   m_filterView = new FilterView(m_viewTabs);
2124   Controller::self()->addObserver(m_filterView);
2125   m_viewTabs->insertTab(1, m_filterView, QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Filters"));
2126   m_filterView->setWhatsThis(i18n("<qt>The <i>Filter View</i> shows the entries which meet certain "
2127                                   "filter rules.</qt>"));
2128 
2129   connect(m_filterView, &FilterView::signalUpdateFilter,
2130           this, &MainWindow::slotUpdateFilter);
2131   // use the EntrySelectionModel as a proxy so when entries get selected in the filter view
2132   // the edit dialog and entry view are updated
2133   // TODO: consider using KSelectionProxyModel
2134   static_cast<EntrySelectionModel*>(m_iconView->selectionModel())->addSelectionProxy(m_filterView->selectionModel());
2135 
2136   // sort by count if column = 1
2137   int sortRole = Config::filterViewSortColumn() == 0 ? static_cast<int>(Qt::DisplayRole) : static_cast<int>(RowCountRole);
2138   Qt::SortOrder sortOrder = Config::filterViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder;
2139   m_filterView->setSorting(sortOrder, sortRole);
2140 }
2141 
addLoanView()2142 void MainWindow::addLoanView() {
2143   if(m_loanView) {
2144     return;
2145   }
2146 
2147   m_loanView = new LoanView(m_viewTabs);
2148   Controller::self()->addObserver(m_loanView);
2149   m_viewTabs->insertTab(2, m_loanView, QIcon::fromTheme(QStringLiteral("kaddressbook")), i18n("Loans"));
2150   m_loanView->setWhatsThis(i18n("<qt>The <i>Loan View</i> shows a list of all the people who "
2151                                 "have borrowed items from your collection.</qt>"));
2152 
2153   // use the EntrySelectionModel as a proxy so when entries get selected in the loan view
2154   // the edit dialog and entry view are updated
2155   // TODO: consider using KSelectionProxyModel
2156   static_cast<EntrySelectionModel*>(m_iconView->selectionModel())->addSelectionProxy(m_loanView->selectionModel());
2157 
2158   // sort by count if column = 1
2159   int sortRole = Config::loanViewSortColumn() == 0 ? static_cast<int>(Qt::DisplayRole) : static_cast<int>(RowCountRole);
2160   Qt::SortOrder sortOrder = Config::loanViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder;
2161   m_loanView->setSorting(sortOrder, sortRole);
2162 }
2163 
updateCaption(bool modified_)2164 void MainWindow::updateCaption(bool modified_) {
2165   QString caption;
2166   if(Data::Document::self()->collection()) {
2167     caption = Data::Document::self()->collection()->title();
2168   }
2169   if(!m_newDocument) {
2170     if(!caption.isEmpty()) {
2171        caption += QLatin1String(" - ");
2172     }
2173     QUrl u = Data::Document::self()->URL();
2174     if(u.isLocalFile() && u.fileName() == i18n(Tellico::untitledFilename)) {
2175       // for new files, the filename is set to Untitled in Data::Document
2176       caption += u.fileName();
2177     } else {
2178       caption += u.toDisplayString(QUrl::PreferLocalFile);
2179     }
2180   }
2181   setCaption(caption, modified_);
2182 }
2183 
slotUpdateToolbarIcons()2184 void MainWindow::slotUpdateToolbarIcons() {
2185   // first change the icon for the menu item
2186   if(Kernel::self()->collectionType() == Data::Collection::Base) {
2187     m_newEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
2188   } else {
2189     m_newEntry->setIcon(QIcon(QLatin1String(":/icons/") + Kernel::self()->collectionTypeName()));
2190   }
2191 }
2192 
slotGroupLabelActivated()2193 void MainWindow::slotGroupLabelActivated() {
2194   // need entry grouping combo id
2195   foreach(QWidget* widget, m_entryGrouping->associatedWidgets()) {
2196     if(::qobject_cast<KToolBar*>(widget)) {
2197       QWidget* container = m_entryGrouping->requestWidget(widget);
2198       QComboBox* combo = ::qobject_cast<QComboBox*>(container); //krazy:exclude=qclasses
2199       if(combo) {
2200         combo->showPopup();
2201         break;
2202       }
2203     }
2204   }
2205 }
2206 
slotFilterLabelActivated()2207 void MainWindow::slotFilterLabelActivated() {
2208   m_quickFilter->setFocus();
2209   m_quickFilter->selectAll();
2210 }
2211 
slotClearFilter()2212 void MainWindow::slotClearFilter() {
2213   m_quickFilter->clear();
2214   slotQueueFilter();
2215 }
2216 
slotRenameCollection()2217 void MainWindow::slotRenameCollection() {
2218   Kernel::self()->renameCollection();
2219 }
2220 
slotImageLocationMismatch()2221 void MainWindow::slotImageLocationMismatch() {
2222   // TODO: having a single image location mismatch should not be reason to completely save the whole document
2223   QTimer::singleShot(0, this, &MainWindow::slotImageLocationChanged);
2224 }
2225 
slotImageLocationChanged()2226 void MainWindow::slotImageLocationChanged() {
2227   if(m_savingImageLocationChange) {
2228     return;
2229   }
2230   m_savingImageLocationChange = true;
2231   Data::Document::self()->slotSetModified();
2232   KMessageBox::information(this, QLatin1String("<qt>") +
2233                                  i18n("Some images are not saved in the configured location. The current file "
2234                                       "must be saved and the images will be transferred to the new location.") +
2235                                  QLatin1String("</qt>"));
2236   fileSave();
2237   m_savingImageLocationChange = false;
2238 }
2239 
updateCollectionActions()2240 void MainWindow::updateCollectionActions() {
2241   if(!Data::Document::self()->collection()) {
2242     return;
2243   }
2244 
2245   stateChanged(QStringLiteral("collection_reset"));
2246 
2247   Data::Collection::Type type = Data::Document::self()->collection()->type();
2248   stateChanged(QLatin1String("is_") + CollectionFactory::typeName(type));
2249 
2250   Controller::self()->updateActions();
2251   // special case when there are no available data sources
2252   if(m_fetchActions.isEmpty() && m_updateAll) {
2253     m_updateAll->setEnabled(false);
2254   }
2255 }
2256 
updateEntrySources()2257 void MainWindow::updateEntrySources() {
2258   unplugActionList(QStringLiteral("update_entry_actions"));
2259   foreach(QAction* action, m_fetchActions) {
2260     foreach(QWidget* widget, action->associatedWidgets()) {
2261       widget->removeAction(action);
2262     }
2263     m_updateMapper->removeMappings(action);
2264   }
2265   qDeleteAll(m_fetchActions);
2266   m_fetchActions.clear();
2267 
2268   Fetch::FetcherVec vec = Fetch::Manager::self()->fetchers(Kernel::self()->collectionType());
2269   foreach(Fetch::Fetcher::Ptr fetcher, vec) {
2270     QAction* action = new QAction(Fetch::Manager::fetcherIcon(fetcher.data()), fetcher->source(), actionCollection());
2271     action->setToolTip(i18n("Update entry data from %1", fetcher->source()));
2272     void (QAction::* triggeredBool)(bool) = &QAction::triggered;
2273     void (QSignalMapper::* mapVoid)() = &QSignalMapper::map;
2274     connect(action, triggeredBool, m_updateMapper, mapVoid);
2275     m_updateMapper->setMapping(action, fetcher->source());
2276     m_fetchActions.append(action);
2277   }
2278 
2279   plugActionList(QStringLiteral("update_entry_actions"), m_fetchActions);
2280 }
2281 
importFile(Tellico::Import::Format format_,const QList<QUrl> & urls_)2282 void MainWindow::importFile(Tellico::Import::Format format_, const QList<QUrl>& urls_) {
2283   QList<QUrl> urls = urls_;
2284   // update as DropHandler and Importer classes are updated
2285   if(urls_.count() > 1 &&
2286      format_ != Import::Bibtex &&
2287      format_ != Import::RIS &&
2288      format_ != Import::CIW &&
2289      format_ != Import::PDF) {
2290     QUrl u = urls_.front();
2291     QString url = u.isLocalFile() ? u.path() : u.toDisplayString();
2292     Kernel::self()->sorry(i18n("Tellico can only import one file of this type at a time. "
2293                                "Only %1 will be imported.", url));
2294     urls.clear();
2295     urls += u;
2296   }
2297 
2298   ImportDialog dlg(format_, urls, this);
2299   if(dlg.exec() != QDialog::Accepted) {
2300     return;
2301   }
2302 
2303 //  if edit dialog is saved ok and if replacing, then the doc is saved ok
2304   if(m_editDialog->queryModified() &&
2305      (dlg.action() != Import::Replace || querySaveModified())) {
2306     GUI::CursorSaver cs(Qt::WaitCursor);
2307     Data::CollPtr coll = dlg.collection();
2308     if(!coll) {
2309       if(!dlg.statusMessage().isEmpty()) {
2310         Kernel::self()->sorry(dlg.statusMessage());
2311       }
2312       return;
2313     }
2314     importCollection(coll, dlg.action());
2315   }
2316 }
2317 
importText(Tellico::Import::Format format_,const QString & text_)2318 void MainWindow::importText(Tellico::Import::Format format_, const QString& text_) {
2319   if(text_.isEmpty()) {
2320     return;
2321   }
2322   Data::CollPtr coll = ImportDialog::importText(format_, text_);
2323   if(coll) {
2324     importCollection(coll, Import::Merge);
2325   }
2326 }
2327 
importCollection(Tellico::Data::CollPtr coll_,Tellico::Import::Action action_)2328 bool MainWindow::importCollection(Tellico::Data::CollPtr coll_, Tellico::Import::Action action_) {
2329   bool failed = false;
2330   switch(action_) {
2331     case Import::Append:
2332       {
2333         // only append if match, but special case importing books into bibliographies
2334         Data::CollPtr c = Data::Document::self()->collection();
2335         if(c->type() == coll_->type()
2336           || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) {
2337           Kernel::self()->appendCollection(coll_);
2338           slotEnableModifiedActions(true);
2339         } else {
2340           Kernel::self()->sorry(i18n(errorAppendType));
2341           failed = true;
2342         }
2343       }
2344       break;
2345 
2346     case Import::Merge:
2347       {
2348         // only merge if match, but special case importing books into bibliographies
2349         Data::CollPtr c = Data::Document::self()->collection();
2350         if(c->type() == coll_->type()
2351           || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) {
2352           Kernel::self()->mergeCollection(coll_);
2353           slotEnableModifiedActions(true);
2354         } else {
2355           Kernel::self()->sorry(i18n(errorMergeType));
2356           failed = true;
2357         }
2358       }
2359       break;
2360 
2361     default: // replace
2362       Kernel::self()->replaceCollection(coll_);
2363       m_fileOpenRecent->setCurrentItem(-1);
2364       m_newDocument = true;
2365       slotEnableOpenedActions();
2366       slotEnableModifiedActions(false);
2367       break;
2368   }
2369   // tell the entry views and models that there are no further images to load
2370   m_detailedView->slotRefreshImages();
2371   return !failed;
2372 }
2373 
slotURLAction(const QUrl & url_)2374 void MainWindow::slotURLAction(const QUrl& url_) {
2375   Q_ASSERT(url_.scheme() == QLatin1String("tc"));
2376   QString actionName = url_.fileName();
2377   QAction* action = this->action(actionName.toLatin1().constData());
2378   if(action) {
2379     action->activate(QAction::Trigger);
2380   } else {
2381     myWarning() << "unknown action: " << actionName;
2382   }
2383 }
2384 
eventFilter(QObject * obj_,QEvent * ev_)2385 bool MainWindow::eventFilter(QObject* obj_, QEvent* ev_) {
2386   if(ev_->type() == QEvent::KeyPress && obj_ == m_quickFilter) {
2387     switch(static_cast<QKeyEvent*>(ev_)->key()) {
2388       case Qt::Key_Escape:
2389         m_quickFilter->clear();
2390         return true;
2391     }
2392   }
2393   return KXmlGuiWindow::eventFilter(obj_, ev_);
2394 }
2395 
slotToggleFullScreen()2396 void MainWindow::slotToggleFullScreen() {
2397   Qt::WindowStates ws = windowState();
2398   setWindowState((ws & Qt::WindowFullScreen) ? (ws & ~Qt::WindowFullScreen) : (ws | Qt::WindowFullScreen));
2399 }
2400 
slotToggleMenuBarVisibility()2401 void MainWindow::slotToggleMenuBarVisibility() {
2402   QMenuBar* mb = menuBar();
2403   mb->isHidden() ? mb->show() : mb->hide();
2404 }
2405 
slotToggleLayoutLock(bool lock_)2406 void MainWindow::slotToggleLayoutLock(bool lock_) {
2407   m_groupViewDock->setLocked(lock_);
2408   m_collectionViewDock->setLocked(lock_);
2409 }
2410 
slotResetLayout()2411 void MainWindow::slotResetLayout() {
2412   removeDockWidget(m_groupViewDock);
2413   addDockWidget(Qt::LeftDockWidgetArea, m_groupViewDock);
2414   m_groupViewDock->show();
2415 
2416   m_dummyWindow->removeDockWidget(m_collectionViewDock);
2417   m_dummyWindow->addDockWidget(Qt::TopDockWidgetArea, m_collectionViewDock);
2418   m_collectionViewDock->show();
2419 }
2420 
guiFactoryReset()2421 void MainWindow::guiFactoryReset() {
2422   guiFactory()->removeClient(this);
2423   guiFactory()->reset();
2424   guiFactory()->addClient(this);
2425 }
2426