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