diff --git a/doc/importing-exporting.docbook b/doc/importing-exporting.docbook index a7ca2b68..9137d64e 100644 --- a/doc/importing-exporting.docbook +++ b/doc/importing-exporting.docbook @@ -1,356 +1,364 @@ Importing and Exporting Data &appname; is able to import and export a wide variety of data files, as well as search various Internet sites for information. Importing from the Internet &appname; is able to search various Internet sites using the Internet Search Dialog. Entries may be directly imported and added to your current collection. The various providers are configured via the Data Sources Options. Searches may use different criteria: Title, Person, ISBN, UPC/EAN, LCCN, or Keyword. Not all criteria are available for certain data sources. ISBN and LCCN values are for books only, while UPC or EAN values can apply to any type of item. Once a search is initiated, the Search button becomes Stop which will end a search. As results are found, they are added to the list directly under the search box, where selecting an item will show the result without adding it to the collection. Clicking the Add Entry button will add all the selected items to your collection. If the data source has more results than were initially requested, the Get More Results button becomes active. Clicking Clear will remove all the current results and reset the search. The &appname; Internet Search Dialog The &appname; Internet Search Dialog Only entries that match the current collection type will be found. The Description column provides additional information about the entry, in order to differentiate between videos in different formats, or books in different bindings, for example. Once an entry is successfully added to the collection, a checkmark is added to the first column in the list. Multiple entries can be added at once by using the standard &kde; method for multiple selection, which usually involves holding the &Shift; or &Ctrl; key when clicking on an item. To facilitate the use of barcode scanners, searches can include multiple ISBN/UPC values. Selecting the Multiple ISBN/UPC search check box will disable the search box and enable the Edit ISBN/UPC values... button, which will open a multi-line text entry box. Each ISBN should be entered on a line by itself. After closing the box, each ISBN will be validated for correct formatting. The ISBN validation is able to convert 13-digit EAN values, as well as full UPC codes, to the proper formatting. The ISBN list may also be read from a text file. Importing Data &appname; offers three different actions when importing data. Replace current collection will close the current collection, and create a new one with the data from the imported file. Append to current collection tells &appname; to add all the entries in the imported collection to the current one, and to add any fields which don't currently exist. The Merge collection action is the same as appending, except that each imported entry is compared to the current ones, and any identical entries are skipped. &appname; attempts to identify matching entries which are not completely identical by comparing significant fields and will then merge the entries. For example, music collections compare the artist and album, and the tracks would be merged for matching entries. The audio file importer is able to correctly build track lists by merging entries. Importing Data From Other Software &appname; can import data directly from a variety of other collection management programs, including GCstar, Alexandria, Delicious Library, Ant Movie Catalog, Referencer, and Griffith. Importing Other Data Formats &appname; can import data from a variety of other file formats, including CSV, bibtex, audio discs and files, MODS, &PDF;, and RIS. Importing &appname; Data Other &appname; data files may be imported directly. Replacing the current collection by importing a &appname; file is the same thing as just opening the file itself. The value of importing &appname; data is primarily for appending or merging two collections together. Importing CSV Data Comma-separated values (CSV) are a common way of importing and exporting tabular data. Each field value is separated by a comma, with one entry per line. The field titles may be included in the first line. The CSV importer is not limited to using a comma as the separator. Any character or string may be used. The CSV Import Dialog The CSV Import Dialog First, select the type of collection that you are importing. If you are appending or merging to your open collection, the type is limited to your current collection type. If the first line of the CSV file contains the field titles, click the check box and the importer will automatically compare the titles against the fields in the current collection. If a field title matches, the header for that column changes to show that the column has been assigned to that field. If the file uses a delimiter other than a comma, be sure to change that option accordingly. In order for &appname; to properly import the file, it must know which field corresponds to each column. If the column only has a number in the header, the data in that column will not be imported. You should assign fields to each column by selecting a column, either by clicking in it or changing the column number itself, then selecting the field to assign from the drop down box and clicking the Assign Field button. If you need to add a new field to the collection, the last item in the drop down box opens the Collection Fields Dialog. For compactness, only the first five lines of the imported CSV file are shown in the dialog. However, all the lines in the file will be imported. Importing Audio &CD; Data &appname; is able to use the freedb.org service to lookup information about a &CD;, including the track list. Depending on your distribution, settings for access to the service may be set in the &kde; &systemsettings;. The &CD; artist, title, genre, year, and track listing are all added. In addition, if the disc contains &CD;-Text, that information is read and added to the imported entry. Importing Audio File Metadata &appname; is able to scan a folder and read the tags for common audio file formats, such as mp3 and ogg. The songs are entered in a music collection, where each entry is an album. If the song files contain the track number, the song name is inserted in the correct spot in the track list. The artist and genre information is also added to the entry. If the song tags contain comments, they are appended to the comments field in the entry, preceded by the file name. In addition, if a folder contains a .directory file and the folder name matches an album title, the Icon entry in the desktop file is used as the cover image for the album. The audio file metadata importer can recursively scan a folder to find all audio files in any subfolder, though symbolic links are not followed. &appname; uses the TagLib library for reading the audio file metadata, and so can import data from any file type that TagLib understands. Importing Bibtex Data Bibtex is a bibliography format used with the LaTeX document preparation system. Various type of bibliographic references may be included in the file. &appname; imports bibtex files as a Bibliographic collection. If the bibtex importer encounters fields in the file which are not in the default bibliography collection, they are added as Simple Text fields, with two exceptions. If the field value contains more than 100 characters, it becomes a Paragraph field. If the field value appears to contain a &URL; or a file reference, then a &URL; field is created. &appname; uses an internal copy of the btparse library for parsing the bibtex files. Bibtexml is an &XML; representation of bibtex data, and the data from the imported bibtexml file is treated in the same way as bibtex data would be. Importing MODS Data MODS is a format for representing various types of media collections. Currently, only books are imported by &appname;, as a Bibliographic collection. Importing &PDF; Data If &appname; was compiled with exempi or poppler support, metadata from &PDF; files can be imported. Metadata may include title, author, and date information, as well as bibliographic identifiers which are then used to update other information. Importing RIS Data The RIS format is a bibliographic file format used by EndNote, Reference Manager, and others. &appname; imports RIS files as a Bibliographic collection. Importing Online Collections &appname; can connect to and import from websites that manage personal collections. Importing BoardGameGeek Collection BoardGameGeek is an online board gaming resource and community. &appname; can import the board games in a user's collection, as long as the collection is set to be publicly accessible. The imported collection may be limited to those items marked as being owned. Importing Goodreads Collection Goodreads is an online social network for readers to track book collections. &appname; can import the list of books in a user's collection, given either the user name or user ID, as long as the collection is set to be publicly accessible. + + +Importing LibraryThing Collection + +LibraryThing is an online service to help people catalog their books easily. &appname; can import the list of books in a user's collection, exported in JSON format. + + + Importing File Listings The best way to create a File Catalog is to import the contents of a folder. The folder may be searched recursively, to add all files found within. This importer is most useful for backup listings and media cataloging, such as &CD; or DVD listings. In addition, image previews of the file contents may be generated, although it can take some time to read a large number of files. The file previews are same as those shown in the &kde; file manager. Importing &XML; Data via XSLT Any &XML; file may be imported into &appname; provided an &XSL; stylesheet is available to convert the file to &appname; format. &appname; automatically loads the stylesheet and performs the &xslt; processing needed to load the file. Drag and Drop Dragging data files to the main &appname; window and dropping them will import the files, just as if the import command was made from the menus. Drag and drop works for the following file formats: Tellico, Bibtex, RIS, and &PDF;. Importing multiple files at once is also supported. So, for example, if you want to catalog several &PDF; files, select them in the file manager and drag them to the &appname; window. &appname; will import as much metadata from the files as it can, and then fetch additional information from various configured Internet sources. Exporting Data When exporting the data in the collection, the entry values may be exported as entered, or with the automatic formatting provided by &appname;. Additionally, the export may be limited to the currently selected entries of the collection as well, where the statusbar shows the number of selected entries. Exported text files, such as Bibtex or CSV, may use the Unicode (UTF-8) character encoding, or the current locale of the operating system. General Export Options General Export Options Exporting &XML; The file format for &appname; data is a zipped &XML; file. Exporting to &XML; merely creates the &XML; file without zipping it. Images may be included in the &XML; file as base64-encoded data in an image element, but doing so can create very large text files. Exporting Zip The standard file format for &appname; is a zipped file, contained the &XML; collection file, and optionally, all the images referenced in the collection. If the images are being stored in the application folder instead, exporting to a Zip file will create a self-contained data file, which includes all the images in the collection. Exporting &HTML; The &HTML; export uses the tellico2html.xsl stylesheet. Images are exported to a folder with the same name as the exported &HTML; file with _files appended. The default format is similar to the printed output, and allows various options for modifying the &HTML;. Field headers may be printed at the top of each column, but unfortunately, &kde; does not yet allow the table headers to be repeated on each page. The entries may be grouped as in the Group View, as well. Additionally, individual files may be created for each entry in the collection, with links created in the top-level &HTML; file. The entry files will be created in the same folder as the images. The entry &HTML; files will use the current stylesheet template, as shown in the Entry View. &HTML; Export Options &HTML; Export Options Exporting CSV Comma-separated values (CSV) are a common way of importing and exporting tabular data. Each field value is separated by a comma, with one entry per line. The field titles may be included as headers in the first line. Any character or string other than a comma may also be used to delimit the fields. CSV Export Options CSV Export Options Exporting Alexandria Alexandria is a book collection manager for the GNOME desktop environment. &appname; is able to export a limited subset of book collection fields to the default Alexandria data location. Exporting ONIX ONIX is an &XML; format for representing and communicating book industry product information, primarily for book vendors. &appname; can export book collections using a small subset of ONIX. Exporting Bibtex When exporting to Bibtex, the field values may be escaped with braces or quotation marks. If any string macros are used in the collection, they may optionally be exported as macros or expanded. For &URL; fields, &appname; may enclose the field values with the \url{...} tag. Finally, entries with no citation key may be skipped rather than have &appname; auto-generate the key. Bibtex Export Options Bibtex Export Options Exporting GCstar GCstar is another movie collection manager. &appname; is able to export most collection types to a GCstar data file. Exporting &XML; via &xslt; Finally, &appname; is able to process its internal &XML; representation of the collection data through an external &XSL; stylesheet before exporting. This type of export may be useful for generating text reports or other file types. Working With Citations When working with a bibliography, citations for the currently selected entries may be generated and used in various other applications. A citation in bibtex format can be copied to the clipboard, and then pasted in a latex file. Bibtex citations can also be pushed to an external application such as LyX or Kile using the so-called lyxpipe. diff --git a/icons/CMakeLists.txt b/icons/CMakeLists.txt index 7016edab..b9d7f85a 100644 --- a/icons/CMakeLists.txt +++ b/icons/CMakeLists.txt @@ -1,82 +1,83 @@ include(ECMInstallIcons) set(PIC_FILES album.png alexandria.png amc.png bibtex.png boardgame.png boardgamegeek.png book.png card.png checkmark.png cite.png coin.png comic.png deliciouslibrary.png file.png game.png gcstar.png - griffith.png goodreads.png + griffith.png + librarything.png nocover_album.png nocover_bibtex.png nocover_boardgame.png nocover_book.png nocover_comic.png nocover_game.png nocover_video.png person-open.png person.png README.icons README.quesnay referencer.png script.png script.svg sru.png stamp.png star_off.png star_on.png stars10.png stars1.png stars2.png stars3.png stars4.png stars5.png stars6.png stars7.png stars8.png stars9.png tellico_mime.svg tellico.png tellico.svg tellico.svg video.png vinoxml.png wine.png ) INSTALL(FILES ${PIC_FILES} DESTINATION ${TELLICO_DATA_INSTALL_DIR}/pics ) set(ICON_FILES 16-apps-tellico.png 16-mimetypes-application-x-tellico.png 22-apps-tellico.png 22-mimetypes-application-x-tellico.png 32-apps-tellico.png 32-mimetypes-application-x-tellico.png 48-apps-tellico.png 48-mimetypes-application-x-tellico.png 64-apps-tellico.png 64-mimetypes-application-x-tellico.png 128-apps-tellico.png 128-mimetypes-application-x-tellico.png ) ecm_install_icons(ICONS ${ICON_FILES} DESTINATION ${KDE_INSTALL_ICONDIR} THEME hicolor ) diff --git a/icons/icons.qrc b/icons/icons.qrc index 6928a582..bcee7435 100644 --- a/icons/icons.qrc +++ b/icons/icons.qrc @@ -1,52 +1,53 @@ album.png alexandria.png amc.png bibtex.png boardgamegeek.png boardgame.png book.png card.png checkmark.png cite.png coin.png comic.png deliciouslibrary.png file.png game.png gcstar.png goodreads.png griffith.png + librarything.png nocover_album.png nocover_bibtex.png nocover_boardgame.png nocover_book.png nocover_comic.png nocover_game.png nocover_video.png person-open.png person.png referencer.png script.png sru.png stamp.png star_off.png star_on.png stars10.png stars1.png stars2.png stars3.png stars4.png stars5.png stars6.png stars7.png stars8.png stars9.png tellico.png video.png vinoxml.png wine.png diff --git a/icons/librarything.png b/icons/librarything.png new file mode 100644 index 00000000..babe0c95 Binary files /dev/null and b/icons/librarything.png differ diff --git a/src/importdialog.cpp b/src/importdialog.cpp index b989a35c..2c4d2798 100644 --- a/src/importdialog.cpp +++ b/src/importdialog.cpp @@ -1,482 +1,490 @@ /*************************************************************************** Copyright (C) 2003-2014 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "importdialog.h" #include "document.h" #include "tellico_debug.h" #include "collection.h" #include "progressmanager.h" #include "utils/guiproxy.h" #include "translators/importer.h" #include "translators/tellicoimporter.h" #include "translators/bibteximporter.h" #include "translators/bibtexmlimporter.h" #include "translators/csvimporter.h" #include "translators/xsltimporter.h" #include "translators/audiofileimporter.h" #include "translators/alexandriaimporter.h" #include "translators/freedbimporter.h" #include "translators/risimporter.h" #include "translators/gcstarimporter.h" #include "translators/filelistingimporter.h" #include "translators/amcimporter.h" #include "translators/griffithimporter.h" #include "translators/pdfimporter.h" #include "translators/referencerimporter.h" #include "translators/deliciousimporter.h" #include "translators/goodreadsimporter.h" #include "translators/ciwimporter.h" #include "translators/vinoxmlimporter.h" #include "translators/boardgamegeekimporter.h" +#include "translators/librarythingimporter.h" #include "utils/datafileregistry.h" #include #include #include #include #include #include #include #include #include #include using Tellico::ImportDialog; ImportDialog::ImportDialog(Tellico::Import::Format format_, const QList& urls_, QWidget* parent_) : QDialog(parent_), m_importer(importer(format_, urls_)) { setModal(true); setWindowTitle(i18n("Import Options")); QVBoxLayout* mainLayout = new QVBoxLayout(); setLayout(mainLayout); QWidget* widget = new QWidget(this); mainLayout->addWidget(widget); QVBoxLayout* topLayout = new QVBoxLayout(widget); QGroupBox* groupBox = new QGroupBox(i18n("Import Options"), widget); QVBoxLayout* vlay = new QVBoxLayout(groupBox); topLayout->addWidget(groupBox, 0); m_radioReplace = new QRadioButton(i18n("&Replace current collection"), groupBox); m_radioReplace->setWhatsThis(i18n("Replace the current collection with the contents " "of the imported file.")); m_radioAppend = new QRadioButton(i18n("A&ppend to current collection"), groupBox); m_radioAppend->setWhatsThis(i18n("Append the contents of the imported file to the " "current collection. This is only possible when the " "collection types match.")); m_radioMerge = new QRadioButton(i18n("&Merge with current collection"), groupBox); m_radioMerge->setWhatsThis(i18n("Merge the contents of the imported file to the " "current collection. This is only possible when the " "collection types match. Entries must match exactly " "in order to be merged.")); if(m_importer->canImport(Data::Document::self()->collection()->type())) { // append by default? m_radioAppend->setChecked(true); } else { m_radioReplace->setChecked(true); m_radioAppend->setEnabled(false); m_radioMerge->setEnabled(false); } vlay->addWidget(m_radioReplace); vlay->addWidget(m_radioAppend); vlay->addWidget(m_radioMerge); m_buttonGroup = new QButtonGroup(widget); m_buttonGroup->addButton(m_radioReplace, Import::Replace); m_buttonGroup->addButton(m_radioAppend, Import::Append); m_buttonGroup->addButton(m_radioMerge, Import::Merge); QWidget* w = m_importer->widget(widget); // m_importer->readOptions(KSharedConfig::openConfig()); if(w) { w->layout()->setMargin(0); topLayout->addWidget(w, 0); } connect(m_buttonGroup, static_cast(&QButtonGroup::buttonClicked), m_importer, &Tellico::Import::Importer::slotActionChanged); topLayout->addStretch(); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); mainLayout->addWidget(buttonBox); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(okButton, &QPushButton::clicked, this, &ImportDialog::slotOk); connect(buttonBox, &QDialogButtonBox::accepted, this, &ImportDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &ImportDialog::reject); KGuiItem ok = KStandardGuiItem::ok(); ok.setText(i18n("&Import")); KGuiItem::assign(okButton, ok); // want to grab default button action, too // since the importer might do something with widgets, don't just call it, do it after layout is done QTimer::singleShot(0, this, &ImportDialog::slotUpdateAction); } ImportDialog::~ImportDialog() { delete m_importer; m_importer = nullptr; } Tellico::Data::CollPtr ImportDialog::collection() { if(m_importer && !m_coll) { ProgressItem& item = ProgressManager::self()->newProgressItem(m_importer, m_importer->progressLabel(), true); connect(m_importer, &Import::Importer::signalTotalSteps, ProgressManager::self(), &ProgressManager::setTotalSteps); connect(m_importer, &Import::Importer::signalProgress, ProgressManager::self(), &ProgressManager::setProgress); connect(&item, &ProgressItem::signalCancelled, m_importer, &Import::Importer::slotCancel); ProgressItem::Done done(m_importer); m_coll = m_importer->collection(); } return m_coll; } QString ImportDialog::statusMessage() const { return m_importer ? m_importer->statusMessage() : QString(); } Tellico::Import::Action ImportDialog::action() const { if(m_radioReplace->isChecked()) { return Import::Replace; } else if(m_radioAppend->isChecked()) { return Import::Append; } else { return Import::Merge; } } // static Tellico::Import::Importer* ImportDialog::importer(Tellico::Import::Format format_, const QList& urls_) { #define CHECK_SIZE if(urls_.size() > 1) myWarning() << "only importing first URL" QUrl firstURL = urls_.isEmpty() ? QUrl() : urls_[0]; Import::Importer* importer = nullptr; switch(format_) { case Import::TellicoXML: CHECK_SIZE; importer = new Import::TellicoImporter(firstURL); break; case Import::Bibtex: importer = new Import::BibtexImporter(urls_); break; case Import::Bibtexml: CHECK_SIZE; importer = new Import::BibtexmlImporter(firstURL); break; case Import::CSV: CHECK_SIZE; importer = new Import::CSVImporter(firstURL); break; case Import::XSLT: CHECK_SIZE; importer = new Import::XSLTImporter(firstURL); break; case Import::MODS: CHECK_SIZE; importer = new Import::XSLTImporter(firstURL); { QString xsltFile = DataFileRegistry::self()->locate(QStringLiteral("mods2tellico.xsl")); if(!xsltFile.isEmpty()) { QUrl u = QUrl::fromLocalFile(xsltFile); static_cast(importer)->setXSLTURL(u); } else { myWarning() << "unable to find mods2tellico.xml!"; } } break; case Import::AudioFile: CHECK_SIZE; importer = new Import::AudioFileImporter(firstURL); break; case Import::Alexandria: CHECK_SIZE; importer = new Import::AlexandriaImporter(); break; case Import::FreeDB: CHECK_SIZE; importer = new Import::FreeDBImporter(); break; case Import::RIS: importer = new Import::RISImporter(urls_); break; case Import::GCstar: CHECK_SIZE; importer = new Import::GCstarImporter(firstURL); break; case Import::FileListing: CHECK_SIZE; importer = new Import::FileListingImporter(firstURL); break; case Import::AMC: CHECK_SIZE; importer = new Import::AMCImporter(firstURL); break; case Import::Griffith: CHECK_SIZE; importer = new Import::GriffithImporter(firstURL); break; case Import::PDF: importer = new Import::PDFImporter(urls_); break; case Import::Referencer: CHECK_SIZE; importer = new Import::ReferencerImporter(firstURL); break; case Import::Delicious: CHECK_SIZE; importer = new Import::DeliciousImporter(firstURL); break; case Import::Goodreads: CHECK_SIZE; importer = new Import::GoodreadsImporter(); break; case Import::GRS1: myDebug() << "GRS1 not implemented"; break; case Import::CIW: importer = new Import::CIWImporter(urls_); break; case Import::VinoXML: CHECK_SIZE; importer = new Import::VinoXMLImporter(firstURL); break; case Import::BoardGameGeek: CHECK_SIZE; importer = new Import::BoardGameGeekImporter(); break; + + case Import::LibraryThing: + CHECK_SIZE; + importer = new Import::LibraryThingImporter(); + break; } if(!importer) { myWarning() << "importer not created!"; return nullptr; } importer->setCurrentCollection(Data::Document::self()->collection()); return importer; #undef CHECK_SIZE } //static Tellico::Import::Importer* ImportDialog::importerForText(Tellico::Import::Format format_, const QString& text_) { Import::Importer* importer = nullptr; switch(format_) { case Import::Bibtex: importer = new Import::BibtexImporter(text_); break; default: break; } if(!importer) { myWarning() << "importer not created!"; return nullptr; } importer->setCurrentCollection(Data::Document::self()->collection()); return importer; } // static QString ImportDialog::fileFilter(Tellico::Import::Format format_) { QString text; switch(format_) { case Import::TellicoXML: text = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)") + QLatin1String(";;"); text += i18n("XML Files") + QLatin1String(" (*.xml)") + QLatin1String(";;"); break; case Import::Bibtex: text = i18n("Bibtex Files") + QLatin1String(" (*.bib)") + QLatin1String(";;"); break; case Import::CSV: text = i18n("CSV Files") + QLatin1String(" (*.csv)") + QLatin1String(";;"); break; case Import::Bibtexml: case Import::XSLT: case Import::MODS: case Import::Delicious: case Import::Griffith: text = i18n("XML Files") + QLatin1String(" (*.xml)") + QLatin1String(";;"); break; case Import::RIS: text = i18n("RIS Files") + QLatin1String(" (*.ris)") + QLatin1String(";;"); break; case Import::GCstar: text = i18n("GCstar Data Files") + QLatin1String(" (*.gcs *.gcf)") + QLatin1String(";;"); break; case Import::AMC: text = i18n("AMC Data Files") + QLatin1String(" (*.amc)") + QLatin1String(";;"); break; case Import::PDF: text = i18n("PDF Files") + QLatin1String(" (*.pdf)") + QLatin1String(";;"); break; case Import::Referencer: text = i18n("Referencer Files") + QLatin1String(" (*.reflib)") + QLatin1String(";;"); break; case Import::CIW: text = i18n("CIW Files") + QLatin1String(" (*.ciw)") + QLatin1String(";;"); break; case Import::VinoXML: text = i18n("VinoXML Data Files") + QLatin1String(" (*.vinoxml)") + QLatin1String(";;"); text += i18n("XML Files") + QLatin1String(" (*.xml)") + QLatin1String(";;"); break; case Import::AudioFile: case Import::Alexandria: case Import::FreeDB: case Import::FileListing: case Import::GRS1: case Import::Goodreads: case Import::BoardGameGeek: + case Import::LibraryThing: break; } return text + i18n("All Files") + QLatin1String(" (*)"); } // audio files are imported by directory // alexandria is a defined location, as is freedb // all others are files Tellico::Import::Target ImportDialog::importTarget(Tellico::Import::Format format_) { switch(format_) { case Import::AudioFile: case Import::FileListing: return Import::Dir; case Import::Alexandria: case Import::FreeDB: case Import::Goodreads: case Import::BoardGameGeek: + case Import::LibraryThing: return Import::None; default: return Import::File; } } QString ImportDialog::startDir(Tellico::Import::Format format_) { if(format_ == Import::GCstar) { QDir dir = QDir::home(); // able to cd if exists and readable if(dir.cd(QStringLiteral(".local/share/gcstar/"))) { return dir.absolutePath(); } } return QString(); } void ImportDialog::slotOk() { // some importers, like the CSV importer, can validate their settings if(!m_importer || m_importer->validImport()) { accept(); } else { myLog() << "not a valid import"; } } void ImportDialog::slotUpdateAction() { m_importer->slotActionChanged(m_buttonGroup->checkedId()); } // static Tellico::Data::CollPtr ImportDialog::importURL(Tellico::Import::Format format_, const QUrl& url_) { Import::Importer* imp = importer(format_, QList() << url_); if(!imp) { return Data::CollPtr(); } ProgressItem& item = ProgressManager::self()->newProgressItem(imp, imp->progressLabel(), true); connect(imp, &Import::Importer::signalTotalSteps, ProgressManager::self(), &ProgressManager::setTotalSteps); connect(imp, &Import::Importer::signalProgress, ProgressManager::self(), &ProgressManager::setProgress); connect(&item, &ProgressItem::signalCancelled, imp, &Import::Importer::slotCancel); ProgressItem::Done done(imp); Data::CollPtr c = imp->collection(); if(!c && !imp->statusMessage().isEmpty()) { GUI::Proxy::sorry(imp->statusMessage()); } delete imp; return c; } Tellico::Data::CollPtr ImportDialog::importText(Tellico::Import::Format format_, const QString& text_) { Import::Importer* imp = importerForText(format_, text_); if(!imp) { return Data::CollPtr(); } // the Done() constructor crashes for some reason, so just don't use it // 5/18/19 -> uncomment the progress Done again ProgressItem& item = ProgressManager::self()->newProgressItem(imp, imp->progressLabel(), true); connect(imp, &Import::Importer::signalTotalSteps, ProgressManager::self(), &ProgressManager::setTotalSteps); connect(imp, &Import::Importer::signalProgress, ProgressManager::self(), &ProgressManager::setProgress); connect(&item, &ProgressItem::signalCancelled, imp, &Import::Importer::slotCancel); ProgressItem::Done done(imp); Data::CollPtr c = imp->collection(); if(!c && !imp->statusMessage().isEmpty()) { GUI::Proxy::sorry(imp->statusMessage()); } delete imp; return c; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8250c562..051afc9a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,2388 +1,2391 @@ /*************************************************************************** Copyright (C) 2001-2014 Robby Stephenson Copyright (C) 2011 Pedro Miguel Carvalho ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "mainwindow.h" #include "tellico_kernel.h" #include "document.h" #include "detailedlistview.h" #include "entryeditdialog.h" #include "groupview.h" #include "viewstack.h" #include "collection.h" #include "collectionfactory.h" #include "entry.h" #include "configdialog.h" #include "filter.h" #include "filterdialog.h" #include "collectionfieldsdialog.h" #include "controller.h" #include "importdialog.h" #include "exportdialog.h" #include "core/filehandler.h" // needed so static mainWindow variable can be set #include "translators/htmlexporter.h" // for printing #include "entryview.h" #include "entryiconview.h" #include "images/imagefactory.h" // needed so tmp files can get cleaned #include "collections/collectioninitializer.h" #include "collections/bibtexcollection.h" // needed for bibtex string macro dialog #include "utils/bibtexhandler.h" // needed for bibtex options #include "utils/datafileregistry.h" #include "fetchdialog.h" #include "reportdialog.h" #include "bibtexkeydialog.h" #include "core/tellico_strings.h" #include "filterview.h" #include "loanview.h" #include "fetch/fetchmanager.h" #include "fetch/fetcherinitializer.h" #include "cite/actionmanager.h" #include "config/tellico_config.h" #include "core/netaccess.h" #include "dbusinterface.h" #include "models/models.h" #include "models/entryiconmodel.h" #include "models/entryselectionmodel.h" #include "newstuff/manager.h" #include "gui/drophandler.h" #include "gui/stringmapdialog.h" #include "gui/lineedit.h" #include "gui/statusbar.h" #include "gui/tabwidget.h" #include "gui/dockwidget.h" #include "utils/cursorsaver.h" #include "utils/guiproxy.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // needed for copy, cut, paste slots #include #include #include #include #include #include namespace { static const int MAIN_WINDOW_MIN_WIDTH = 600; static const int MAX_IMAGES_WARN_PERFORMANCE = 200; QIcon mimeIcon(const char* s) { QMimeDatabase db; QMimeType ptr = db.mimeTypeForName(QLatin1String(s)); if(!ptr.isValid()) { myDebug() << "*** no icon for" << s; } return ptr.isValid() ? QIcon::fromTheme(ptr.iconName()) : QIcon(); } QIcon mimeIcon(const char* s1, const char* s2) { QMimeDatabase db; QMimeType ptr = db.mimeTypeForName(QLatin1String(s1)); if(!ptr.isValid()) { ptr = db.mimeTypeForName(QLatin1String(s2)); if(!ptr.isValid()) { myDebug() << "*** no icon for" << s1 << "or" << s2; } } return ptr.isValid() ? QIcon::fromTheme(ptr.iconName()) : QIcon(); } } using namespace Tellico; using Tellico::MainWindow; MainWindow::MainWindow(QWidget* parent_/*=0*/) : KXmlGuiWindow(parent_), m_updateAll(nullptr), m_statusBar(nullptr), m_editDialog(nullptr), m_groupView(nullptr), m_filterView(nullptr), m_loanView(nullptr), m_configDlg(nullptr), m_filterDlg(nullptr), m_collFieldsDlg(nullptr), m_stringMacroDlg(nullptr), m_bibtexKeyDlg(nullptr), m_fetchDlg(nullptr), m_reportDlg(nullptr), m_queuedFilters(0), m_initialized(false), m_newDocument(true), m_dontQueueFilter(false), m_savingImageLocationChange(false) { Controller::init(this); // the only time this is ever called! // has to be after controller init Kernel::init(this); // the only time this is ever called! GUI::Proxy::setMainWidget(this); setWindowIcon(QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico")))); // initialize the status bar and progress bar initStatusBar(); // initialize all the collection types // which must be done before the document is created CollectionInitializer initCollections; // register all the fetcher types Fetch::FetcherInitializer initFetchers; // create a document, which also creates an empty book collection // must be done before the different widgets are created initDocument(); // set up all the actions, some connect to the document, so this must be after initDocument() initActions(); // create the different widgets in the view, some widgets connect to actions, so must be after initActions() initView(); // The edit dialog is not created until after the main window is initialized, so it can be a child. // So don't make any connections, don't read options for it until initFileOpen readOptions(); setAcceptDrops(true); DropHandler* drophandler = new DropHandler(this); installEventFilter(drophandler); new ApplicationInterface(this); new CollectionInterface(this); MARK_LINE; QTimer::singleShot(0, this, &MainWindow::slotInit); } MainWindow::~MainWindow() { qDeleteAll(m_fetchActions); m_fetchActions.clear(); } void MainWindow::slotInit() { MARK; // if the edit dialog exists, we know we've already called this function if(m_editDialog) { return; } m_editDialog = new EntryEditDialog(this); Controller::self()->addObserver(m_editDialog); m_toggleEntryEditor->setChecked(Config::showEditWidget()); slotToggleEntryEditor(); m_lockLayout->setActive(Config::lockLayout()); initConnections(); connect(ImageFactory::self(), &ImageFactory::imageLocationMismatch, this, &MainWindow::slotImageLocationMismatch); // Init DBUS NewStuff::Manager::self(); } void MainWindow::initStatusBar() { MARK; m_statusBar = new Tellico::StatusBar(this); setStatusBar(m_statusBar); } void MainWindow::initActions() { MARK; /************************************************* * File->New menu *************************************************/ QSignalMapper* collectionMapper = new QSignalMapper(this); void (QSignalMapper::* mappedInt)(int) = &QSignalMapper::mapped; connect(collectionMapper, mappedInt, this, &MainWindow::slotFileNew); KActionMenu* fileNewMenu = new KActionMenu(i18n("New"), this); fileNewMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); fileNewMenu->setToolTip(i18n("Create a new collection")); fileNewMenu->setDelayed(false); actionCollection()->addAction(QStringLiteral("file_new_collection"), fileNewMenu); QAction* action; void (QSignalMapper::* mapVoid)() = &QSignalMapper::map; #define COLL_ACTION(TYPE, NAME, TEXT, TIP, ICON) \ action = actionCollection()->addAction(QStringLiteral(NAME), collectionMapper, mapVoid); \ action->setText(TEXT); \ action->setToolTip(TIP); \ action->setIcon(QIcon(QStringLiteral(":/icons/" ICON))); \ fileNewMenu->addAction(action); \ collectionMapper->setMapping(action, Data::Collection::TYPE); COLL_ACTION(Book, "new_book_collection", i18n("New &Book Collection"), i18n("Create a new book collection"), "book"); COLL_ACTION(Bibtex, "new_bibtex_collection", i18n("New B&ibliography"), i18n("Create a new bibtex bibliography"), "bibtex"); COLL_ACTION(ComicBook, "new_comic_book_collection", i18n("New &Comic Book Collection"), i18n("Create a new comic book collection"), "comic"); COLL_ACTION(Video, "new_video_collection", i18n("New &Video Collection"), i18n("Create a new video collection"), "video"); COLL_ACTION(Album, "new_music_collection", i18n("New &Music Collection"), i18n("Create a new music collection"), "album"); COLL_ACTION(Coin, "new_coin_collection", i18n("New C&oin Collection"), i18n("Create a new coin collection"), "coin"); COLL_ACTION(Stamp, "new_stamp_collection", i18n("New &Stamp Collection"), i18n("Create a new stamp collection"), "stamp"); COLL_ACTION(Card, "new_card_collection", i18n("New C&ard Collection"), i18n("Create a new trading card collection"), "card"); COLL_ACTION(Wine, "new_wine_collection", i18n("New &Wine Collection"), i18n("Create a new wine collection"), "wine"); COLL_ACTION(Game, "new_game_collection", i18n("New &Game Collection"), i18n("Create a new game collection"), "game"); COLL_ACTION(BoardGame, "new_boardgame_collection", i18n("New Boa&rd Game Collection"), i18n("Create a new board game collection"), "boardgame"); COLL_ACTION(File, "new_file_catalog", i18n("New &File Catalog"), i18n("Create a new file catalog"), "file"); action = actionCollection()->addAction(QStringLiteral("new_custom_collection"), collectionMapper, mapVoid); action->setText(i18n("New C&ustom Collection")); action->setToolTip(i18n("Create a new custom collection")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); fileNewMenu->addAction(action); collectionMapper->setMapping(action, Data::Collection::Base); #undef COLL_ACTION /************************************************* * File menu *************************************************/ action = KStandardAction::open(this, SLOT(slotFileOpen()), actionCollection()); action->setToolTip(i18n("Open an existing document")); m_fileOpenRecent = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(const QUrl&)), actionCollection()); m_fileOpenRecent->setToolTip(i18n("Open a recently used file")); m_fileSave = KStandardAction::save(this, SLOT(slotFileSave()), actionCollection()); m_fileSave->setToolTip(i18n("Save the document")); action = KStandardAction::saveAs(this, SLOT(slotFileSaveAs()), actionCollection()); action->setToolTip(i18n("Save the document as a different file...")); action = KStandardAction::print(this, SLOT(slotFilePrint()), actionCollection()); { KHTMLPart w; // KHTMLPart printing was broken in KDE until KHTML 5.16 const QString version = w.componentData().version(); const uint major = version.section(QLatin1Char('.'), 0, 0).toUInt(); const uint minor = version.section(QLatin1Char('.'), 1, 1).toUInt(); if(major == 5 && minor < 16) { myWarning() << "Printing is broken for KDE Frameworks < 5.16. Please upgrade"; action->setEnabled(false); } } action->setToolTip(i18n("Print the contents of the document...")); action = KStandardAction::quit(this, SLOT(slotFileQuit()), actionCollection()); action->setToolTip(i18n("Quit the application")); /**************** Import Menu ***************************/ QSignalMapper* importMapper = new QSignalMapper(this); connect(importMapper, mappedInt, this, &MainWindow::slotFileImport); KActionMenu* importMenu = new KActionMenu(i18n("&Import"), this); importMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-import"))); importMenu->setToolTip(i18n("Import the collection data from other formats")); importMenu->setDelayed(false); actionCollection()->addAction(QStringLiteral("file_import"), importMenu); #define IMPORT_ACTION(TYPE, NAME, TEXT, TIP, ICON) \ action = actionCollection()->addAction(QStringLiteral(NAME), importMapper, mapVoid); \ action->setText(TEXT); \ action->setToolTip(TIP); \ action->setIcon(ICON); \ importMenu->addAction(action); \ importMapper->setMapping(action, TYPE); IMPORT_ACTION(Import::TellicoXML, "file_import_tellico", i18n("Import Tellico Data..."), i18n("Import another Tellico data file"), QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico")))); IMPORT_ACTION(Import::CSV, "file_import_csv", i18n("Import CSV Data..."), i18n("Import a CSV file"), mimeIcon("text/csv", "text/x-csv")); IMPORT_ACTION(Import::MODS, "file_import_mods", i18n("Import MODS Data..."), i18n("Import a MODS data file"), mimeIcon("text/xml")); IMPORT_ACTION(Import::Alexandria, "file_import_alexandria", i18n("Import Alexandria Data..."), i18n("Import data from the Alexandria book collection manager"), QIcon::fromTheme(QStringLiteral("alexandria"), QIcon(QLatin1String(":/icons/alexandria")))); IMPORT_ACTION(Import::Delicious, "file_import_delicious", i18n("Import Delicious Library Data..."), i18n("Import data from Delicious Library"), QIcon::fromTheme(QStringLiteral("deliciouslibrary"), QIcon(QLatin1String(":/icons/deliciouslibrary")))); IMPORT_ACTION(Import::Referencer, "file_import_referencer", i18n("Import Referencer Data..."), i18n("Import data from Referencer"), QIcon::fromTheme(QStringLiteral("referencer"), QIcon(QLatin1String(":/icons/referencer")))); IMPORT_ACTION(Import::Bibtex, "file_import_bibtex", i18n("Import Bibtex Data..."), i18n("Import a bibtex bibliography file"), mimeIcon("text/x-bibtex")); IMPORT_ACTION(Import::Bibtexml, "file_import_bibtexml", i18n("Import Bibtexml Data..."), i18n("Import a Bibtexml bibliography file"), mimeIcon("text/xml")); IMPORT_ACTION(Import::RIS, "file_import_ris", i18n("Import RIS Data..."), i18n("Import an RIS reference file"), QIcon::fromTheme(QStringLiteral(":/icons/cite"))); IMPORT_ACTION(Import::Goodreads, "file_import_goodreads", i18n("Import Goodreads Collection..."), i18n("Import a collection from Goodreads.com"), QIcon::fromTheme(QStringLiteral(":/icons/goodreads"))); + IMPORT_ACTION(Import::LibraryThing, "file_import_librarything", i18n("Import LibraryThing Collection..."), + i18n("Import a collection from LibraryThing.com"), QIcon::fromTheme(QStringLiteral(":/icons/librarything"))); + IMPORT_ACTION(Import::PDF, "file_import_pdf", i18n("Import PDF File..."), i18n("Import a PDF file"), mimeIcon("application/pdf")); IMPORT_ACTION(Import::AudioFile, "file_import_audiofile", i18n("Import Audio File Metadata..."), i18n("Import meta-data from audio files"), mimeIcon("audio/mp3", "audio/x-mp3")); #ifndef HAVE_TAGLIB action->setEnabled(false); #endif IMPORT_ACTION(Import::FreeDB, "file_import_freedb", i18n("Import Audio CD Data..."), i18n("Import audio CD information"), mimeIcon("media/audiocd", "application/x-cda")); #if !defined (HAVE_KCDDB) && !defined (HAVE_KF5KCDDB) action->setEnabled(false); #endif IMPORT_ACTION(Import::GCstar, "file_import_gcstar", i18n("Import GCstar Data..."), i18n("Import a GCstar data file"), QIcon::fromTheme(QStringLiteral("gcstar"), QIcon(QLatin1String(":/icons/gcstar")))); IMPORT_ACTION(Import::Griffith, "file_import_griffith", i18n("Import Griffith Data..."), i18n("Import a Griffith database"), QIcon::fromTheme(QStringLiteral("griffith"), QIcon(QLatin1String(":/icons/griffith")))); IMPORT_ACTION(Import::AMC, "file_import_amc", i18n("Import Ant Movie Catalog Data..."), i18n("Import an Ant Movie Catalog data file"), QIcon::fromTheme(QStringLiteral("amc"), QIcon(QLatin1String(":/icons/amc")))); IMPORT_ACTION(Import::BoardGameGeek, "file_import_boardgamegeek", i18n("Import BoardGameGeek Collection..."), i18n("Import a collection from BoardGameGeek.com"), QIcon(QLatin1String(":/icons/boardgamegeek"))); IMPORT_ACTION(Import::VinoXML, "file_import_vinoxml", i18n("Import VinoXML..."), i18n("Import VinoXML data"), QIcon(QLatin1String(":/icons/vinoxml"))); IMPORT_ACTION(Import::FileListing, "file_import_filelisting", i18n("Import File Listing..."), i18n("Import information about files in a folder"), mimeIcon("inode/directory")); IMPORT_ACTION(Import::XSLT, "file_import_xslt", i18n("Import XSL Transform..."), i18n("Import using an XSL Transform"), mimeIcon("application/xslt+xml", "text/x-xslt")); #undef IMPORT_ACTION /**************** Export Menu ***************************/ QSignalMapper* exportMapper = new QSignalMapper(this); connect(exportMapper, mappedInt, this, &MainWindow::slotFileExport); KActionMenu* exportMenu = new KActionMenu(i18n("&Export"), this); exportMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); exportMenu->setToolTip(i18n("Export the collection data to other formats")); exportMenu->setDelayed(false); actionCollection()->addAction(QStringLiteral("file_export"), exportMenu); #define EXPORT_ACTION(TYPE, NAME, TEXT, TIP, ICON) \ action = actionCollection()->addAction(QStringLiteral(NAME), exportMapper, mapVoid); \ action->setText(TEXT); \ action->setToolTip(TIP); \ action->setIcon(ICON); \ exportMenu->addAction(action); \ exportMapper->setMapping(action, TYPE); EXPORT_ACTION(Export::TellicoXML, "file_export_xml", i18n("Export to XML..."), i18n("Export to a Tellico XML file"), QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QStringLiteral(":/icons/tellico")))); EXPORT_ACTION(Export::TellicoZip, "file_export_zip", i18n("Export to Zip..."), i18n("Export to a Tellico Zip file"), QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QStringLiteral(":/icons/tellico")))); EXPORT_ACTION(Export::HTML, "file_export_html", i18n("Export to HTML..."), i18n("Export to an HTML file"), mimeIcon("text/html")); EXPORT_ACTION(Export::CSV, "file_export_csv", i18n("Export to CSV..."), i18n("Export to a comma-separated values file"), mimeIcon("text/csv", "text/x-csv")); EXPORT_ACTION(Export::Alexandria, "file_export_alexandria", i18n("Export to Alexandria..."), i18n("Export to an Alexandria library"), QIcon::fromTheme(QStringLiteral("alexandria"), QIcon(QStringLiteral(":/icons/alexandria")))); EXPORT_ACTION(Export::Bibtex, "file_export_bibtex", i18n("Export to Bibtex..."), i18n("Export to a bibtex file"), mimeIcon("text/x-bibtex")); EXPORT_ACTION(Export::Bibtexml, "file_export_bibtexml", i18n("Export to Bibtexml..."), i18n("Export to a Bibtexml file"), mimeIcon("text/xml")); EXPORT_ACTION(Export::ONIX, "file_export_onix", i18n("Export to ONIX..."), i18n("Export to an ONIX file"), mimeIcon("text/xml")); EXPORT_ACTION(Export::GCstar, "file_export_gcstar", i18n("Export to GCstar..."), i18n("Export to a GCstar data file"), QIcon::fromTheme(QStringLiteral("gcstar"), QIcon(QStringLiteral(":/icons/gcstar")))); EXPORT_ACTION(Export::XSLT, "file_export_xslt", i18n("Export XSL Transform..."), i18n("Export using an XSL Transform"), mimeIcon("application/xslt+xml", "text/x-xslt")); #undef EXPORT_ACTION /************************************************* * Edit menu *************************************************/ KStandardAction::undo(Kernel::self()->commandHistory(), SLOT(undo()), actionCollection()); KStandardAction::redo(Kernel::self()->commandHistory(), SLOT(undo()), actionCollection()); action = KStandardAction::cut(this, SLOT(slotEditCut()), actionCollection()); action->setToolTip(i18n("Cut the selected text and puts it in the clipboard")); action = KStandardAction::copy(this, SLOT(slotEditCopy()), actionCollection()); action->setToolTip(i18n("Copy the selected text to the clipboard")); action = KStandardAction::paste(this, SLOT(slotEditPaste()), actionCollection()); action->setToolTip(i18n("Paste the clipboard contents")); action = KStandardAction::selectAll(this, SLOT(slotEditSelectAll()), actionCollection()); action->setToolTip(i18n("Select all the entries in the collection")); action = KStandardAction::deselect(this, SLOT(slotEditDeselect()), actionCollection()); action->setToolTip(i18n("Deselect all the entries in the collection")); action = actionCollection()->addAction(QStringLiteral("edit_search_internet"), this, SLOT(slotShowFetchDialog())); action->setText(i18n("Internet Search...")); action->setIconText(i18n("Search")); // find a better word for this? action->setIcon(QIcon::fromTheme(QStringLiteral("tools-wizard"))); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_I); action->setToolTip(i18n("Search the internet...")); action = actionCollection()->addAction(QStringLiteral("filter_dialog"), this, SLOT(slotShowFilterDialog())); action->setText(i18n("Advanced &Filter...")); action->setIconText(i18n("Filter")); action->setIcon(QIcon::fromTheme(QStringLiteral("view-filter"))); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_J); action->setToolTip(i18n("Filter the collection")); /************************************************* * Collection menu *************************************************/ m_newEntry = actionCollection()->addAction(QStringLiteral("coll_new_entry"), this, SLOT(slotNewEntry())); m_newEntry->setText(i18n("&New Entry...")); m_newEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_newEntry->setIconText(i18n("New")); actionCollection()->setDefaultShortcut(m_newEntry, Qt::CTRL + Qt::Key_N); m_newEntry->setToolTip(i18n("Create a new entry")); m_editEntry = actionCollection()->addAction(QStringLiteral("coll_edit_entry"), this, SLOT(slotShowEntryEditor())); m_editEntry->setText(i18n("&Edit Entry...")); m_editEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); actionCollection()->setDefaultShortcut(m_editEntry, Qt::CTRL + Qt::Key_E); m_editEntry->setToolTip(i18n("Edit the selected entries")); m_copyEntry = actionCollection()->addAction(QStringLiteral("coll_copy_entry"), Controller::self(), SLOT(slotCopySelectedEntries())); m_copyEntry->setText(i18n("D&uplicate Entry")); m_copyEntry->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); actionCollection()->setDefaultShortcut(m_copyEntry, Qt::CTRL + Qt::Key_Y); m_copyEntry->setToolTip(i18n("Copy the selected entries")); m_deleteEntry = actionCollection()->addAction(QStringLiteral("coll_delete_entry"), Controller::self(), SLOT(slotDeleteSelectedEntries())); m_deleteEntry->setText(i18n("&Delete Entry")); m_deleteEntry->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); actionCollection()->setDefaultShortcut(m_deleteEntry, Qt::CTRL + Qt::Key_D); m_deleteEntry->setToolTip(i18n("Delete the selected entries")); m_mergeEntry = actionCollection()->addAction(QStringLiteral("coll_merge_entry"), Controller::self(), SLOT(slotMergeSelectedEntries())); m_mergeEntry->setText(i18n("&Merge Entries")); m_mergeEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-import"))); // CTRL+G is ambiguous, pick another // actionCollection()->setDefaultShortcut(m_mergeEntry, Qt::CTRL + Qt::Key_G); m_mergeEntry->setToolTip(i18n("Merge the selected entries")); m_mergeEntry->setEnabled(false); // gets enabled when more than 1 entry is selected m_checkOutEntry = actionCollection()->addAction(QStringLiteral("coll_checkout"), Controller::self(), SLOT(slotCheckOut())); m_checkOutEntry->setText(i18n("Check-&out...")); m_checkOutEntry->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up-double"))); m_checkOutEntry->setToolTip(i18n("Check-out the selected items")); m_checkInEntry = actionCollection()->addAction(QStringLiteral("coll_checkin"), Controller::self(), SLOT(slotCheckIn())); m_checkInEntry->setText(i18n("Check-&in")); m_checkInEntry->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down-double"))); m_checkInEntry->setToolTip(i18n("Check-in the selected items")); action = actionCollection()->addAction(QStringLiteral("coll_rename_collection"), this, SLOT(slotRenameCollection())); action->setText(i18n("&Rename Collection...")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_R); action->setToolTip(i18n("Rename the collection")); action = actionCollection()->addAction(QStringLiteral("coll_fields"), this, SLOT(slotShowCollectionFieldsDialog())); action->setText(i18n("Collection &Fields...")); action->setIconText(i18n("Fields")); action->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_U); action->setToolTip(i18n("Modify the collection fields")); action = actionCollection()->addAction(QStringLiteral("coll_reports"), this, SLOT(slotShowReportDialog())); action->setText(i18n("&Generate Reports...")); action->setIconText(i18n("Reports")); action->setIcon(QIcon::fromTheme(QStringLiteral("text-rdf"))); action->setToolTip(i18n("Generate collection reports")); action = actionCollection()->addAction(QStringLiteral("coll_convert_bibliography"), this, SLOT(slotConvertToBibliography())); action->setText(i18n("Convert to &Bibliography")); action->setIcon(QIcon(QLatin1String(":/icons/bibtex"))); action->setToolTip(i18n("Convert a book collection to a bibliography")); action = actionCollection()->addAction(QStringLiteral("coll_string_macros"), this, SLOT(slotShowStringMacroDialog())); action->setText(i18n("String &Macros...")); action->setIcon(QIcon::fromTheme(QStringLiteral("view-list-text"))); action->setToolTip(i18n("Edit the bibtex string macros")); action = actionCollection()->addAction(QStringLiteral("coll_key_manager"), this, SLOT(slotShowBibtexKeyDialog())); action->setText(i18n("Check for Duplicate Keys...")); action->setIcon(mimeIcon("text/x-bibtex")); action->setToolTip(i18n("Check for duplicate citation keys")); QSignalMapper* citeMapper = new QSignalMapper(this); connect(citeMapper, mappedInt, this, &MainWindow::slotCiteEntry); action = actionCollection()->addAction(QStringLiteral("cite_clipboard"), citeMapper, mapVoid); action->setText(i18n("Copy Bibtex to Cli&pboard")); action->setToolTip(i18n("Copy bibtex citations to the clipboard")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); citeMapper->setMapping(action, Cite::CiteClipboard); action = actionCollection()->addAction(QStringLiteral("cite_lyxpipe"), citeMapper, mapVoid); action->setText(i18n("Cite Entry in &LyX")); action->setToolTip(i18n("Cite the selected entries in LyX")); action->setIcon(QIcon::fromTheme(QStringLiteral("lyx"), QIcon(QLatin1String(":/icons/lyx")))); citeMapper->setMapping(action, Cite::CiteLyxpipe); m_updateMapper = new QSignalMapper(this); void (QSignalMapper::* mappedString)(const QString&) = &QSignalMapper::mapped; connect(m_updateMapper, mappedString, Controller::self(), &Controller::slotUpdateSelectedEntries); m_updateEntryMenu = new KActionMenu(i18n("&Update Entry"), this); m_updateEntryMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); m_updateEntryMenu->setIconText(i18nc("Update Entry", "Update")); m_updateEntryMenu->setDelayed(false); actionCollection()->addAction(QStringLiteral("coll_update_entry"), m_updateEntryMenu); m_updateAll = actionCollection()->addAction(QStringLiteral("update_entry_all"), m_updateMapper, mapVoid); m_updateAll->setText(i18n("All Sources")); m_updateAll->setToolTip(i18n("Update entry data from all available sources")); m_updateMapper->setMapping(m_updateAll, QStringLiteral("_all")); /************************************************* * Settings menu *************************************************/ setStandardToolBarMenuEnabled(true); createStandardStatusBarAction(); m_lockLayout = new KDualAction(this); connect(m_lockLayout, &KDualAction::activeChanged, this, &MainWindow::slotToggleLayoutLock); m_lockLayout->setActiveText(i18n("Unlock Layout")); m_lockLayout->setActiveToolTip(i18n("Unlock the window's layout")); m_lockLayout->setActiveIcon(QIcon::fromTheme(QStringLiteral("object-unlocked"))); m_lockLayout->setInactiveText(i18n("Lock Layout")); m_lockLayout->setInactiveToolTip(i18n("Lock the window's layout")); m_lockLayout->setInactiveIcon(QIcon::fromTheme(QStringLiteral("object-locked"))); actionCollection()->addAction(QStringLiteral("lock_layout"), m_lockLayout); action = actionCollection()->addAction(QStringLiteral("reset_layout"), this, SLOT(slotResetLayout())); action->setText(i18n("Reset Layout")); action->setToolTip(i18n("Reset the window's layout")); action->setIcon(QIcon::fromTheme(QStringLiteral("resetview"))); m_toggleEntryEditor = new KToggleAction(i18n("Entry &Editor"), this); connect(m_toggleEntryEditor, &QAction::triggered, this, &MainWindow::slotToggleEntryEditor); m_toggleEntryEditor->setToolTip(i18n("Enable/disable the editor")); actionCollection()->addAction(QStringLiteral("toggle_edit_widget"), m_toggleEntryEditor); KStandardAction::preferences(this, SLOT(slotShowConfigDialog()), actionCollection()); /************************************************* * Help menu *************************************************/ KStandardAction::tipOfDay(this, SLOT(slotShowTipOfDay()), actionCollection()); /************************************************* * Short cuts *************************************************/ KStandardAction::fullScreen(this, SLOT(slotToggleFullScreen()), this, actionCollection()); KStandardAction::showMenubar(this, SLOT(slotToggleMenuBarVisibility()), actionCollection()); /************************************************* * Collection Toolbar *************************************************/ action = actionCollection()->addAction(QStringLiteral("change_entry_grouping_accel"), this, SLOT(slotGroupLabelActivated())); action->setText(i18n("Change Grouping")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_G); m_entryGrouping = new KSelectAction(i18n("&Group Selection"), this); m_entryGrouping->setToolTip(i18n("Change the grouping of the collection")); void (KSelectAction::* triggeredInt)(int) = &KSelectAction::triggered; connect(m_entryGrouping, triggeredInt, this, &MainWindow::slotChangeGrouping); actionCollection()->addAction(QStringLiteral("change_entry_grouping"), m_entryGrouping); action = actionCollection()->addAction(QStringLiteral("quick_filter_accel"), this, SLOT(slotFilterLabelActivated())); action->setText(i18n("Filter")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F); m_quickFilter = new GUI::LineEdit(this); m_quickFilter->setPlaceholderText(i18n("Filter here...")); // same text as kdepim and amarok m_quickFilter->setClearButtonEnabled(true); // same as Dolphin text edit m_quickFilter->setMinimumWidth(150); m_quickFilter->setMaximumWidth(300); // want to update every time the filter text changes connect(m_quickFilter, &QLineEdit::textChanged, this, &MainWindow::slotQueueFilter); connect(m_quickFilter, &KLineEdit::clearButtonClicked, this, &MainWindow::slotClearFilter); m_quickFilter->installEventFilter(this); // intercept keyEvents QWidgetAction* widgetAction = new QWidgetAction(this); widgetAction->setDefaultWidget(m_quickFilter); widgetAction->setText(i18n("Filter")); widgetAction->setToolTip(i18n("Filter the collection")); widgetAction->setProperty("isShortcutConfigurable", false); actionCollection()->addAction(QStringLiteral("quick_filter"), widgetAction); // final GUI setup is in initView() } #undef mimeIcon void MainWindow::initDocument() { MARK; Data::Document* doc = Data::Document::self(); Kernel::self()->resetHistory(); KConfigGroup config(KSharedConfig::openConfig(), "General Options"); doc->setLoadAllImages(config.readEntry("Load All Images", false)); // allow status messages from the document connect(doc, &Data::Document::signalStatusMsg, this, &MainWindow::slotStatusMsg); // do stuff that changes when the doc is modified connect(doc, &Data::Document::signalModified, this, &MainWindow::slotEnableModifiedActions); connect(doc, &Data::Document::signalCollectionAdded, Controller::self(), &Controller::slotCollectionAdded); connect(doc, &Data::Document::signalCollectionDeleted, Controller::self(), &Controller::slotCollectionDeleted); connect(Kernel::self()->commandHistory(), &QUndoStack::cleanChanged, doc, &Data::Document::slotSetClean); } void MainWindow::initView() { MARK; // initialize the image factory before the entry models are created ImageFactory::init(); m_entryView = new EntryView(this); connect(m_entryView, &EntryView::signalAction, this, &MainWindow::slotURLAction); m_entryView->view()->setWhatsThis(i18n("The Entry View shows a formatted view of the entry's contents.")); // trick to make sure the group views always extend along the entire left or right side // using QMainWindow::setCorner does not seem to work // 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 m_dummyWindow = new QMainWindow(this); m_dummyWindow->setCentralWidget(m_entryView->view()); m_dummyWindow->setWindowFlags(Qt::Widget); setCentralWidget(m_dummyWindow); m_collectionViewDock = new GUI::DockWidget(i18n("Collection View"), m_dummyWindow); m_collectionViewDock->setObjectName(QStringLiteral("collection_dock")); m_viewStack = new ViewStack(this); m_detailedView = m_viewStack->listView(); Controller::self()->addObserver(m_detailedView); m_detailedView->setWhatsThis(i18n("The Column View shows the value of multiple fields " "for each entry.")); connect(Data::Document::self(), &Data::Document::signalCollectionImagesLoaded, m_detailedView, &DetailedListView::slotRefreshImages); m_iconView = m_viewStack->iconView(); EntryIconModel* iconModel = new EntryIconModel(m_iconView); iconModel->setSourceModel(m_detailedView->model()); m_iconView->setModel(iconModel); Controller::self()->addObserver(m_iconView); m_iconView->setWhatsThis(i18n("The Icon View shows each entry in the collection or group using " "an icon, which may be an image in the entry.")); m_collectionViewDock->setWidget(m_viewStack); m_dummyWindow->addDockWidget(Qt::TopDockWidgetArea, m_collectionViewDock); actionCollection()->addAction(QStringLiteral("toggle_column_widget"), m_collectionViewDock->toggleViewAction()); m_groupViewDock = new GUI::DockWidget(i18n("Group View"), this); m_groupViewDock->setObjectName(QStringLiteral("group_dock")); m_groupViewDock->setAllowedAreas(Qt::DockWidgetAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea)); m_viewTabs = new GUI::TabWidget(this); m_viewTabs->setTabBarHidden(true); m_viewTabs->setDocumentMode(true); m_groupView = new GroupView(m_viewTabs); Controller::self()->addObserver(m_groupView); m_viewTabs->addTab(m_groupView, QIcon::fromTheme(QStringLiteral("folder")), i18n("Groups")); m_groupView->setWhatsThis(i18n("The Group View sorts the entries into groupings " "based on a selected field.")); m_groupViewDock->setWidget(m_viewTabs); addDockWidget(Qt::LeftDockWidgetArea, m_groupViewDock); actionCollection()->addAction(QStringLiteral("toggle_group_widget"), m_groupViewDock->toggleViewAction()); EntrySelectionModel* proxySelect = new EntrySelectionModel(m_iconView->model(), m_detailedView->selectionModel(), this); m_iconView->setSelectionModel(proxySelect); setMinimumWidth(MAIN_WINDOW_MIN_WIDTH); // setting up GUI now rather than in initActions setupGUI(Keys | ToolBar); createGUI(); } void MainWindow::initConnections() { // have to toggle the menu item if the dialog gets closed connect(m_editDialog, &QDialog::finished, this, &MainWindow::slotEditDialogFinished); EntrySelectionModel* proxySelect = static_cast(m_iconView->selectionModel()); connect(proxySelect, &EntrySelectionModel::entriesSelected, Controller::self(), &Controller::slotUpdateSelection); connect(proxySelect, &EntrySelectionModel::entriesSelected, m_editDialog, &EntryEditDialog::setContents); connect(proxySelect, &EntrySelectionModel::entriesSelected, m_entryView, &EntryView::showEntries); // let the group view call filters, too connect(m_groupView, &GroupView::signalUpdateFilter, this, &MainWindow::slotUpdateFilter); // use the EntrySelectionModel as a proxy so when entries get selected in the group view // the edit dialog and entry view are updated proxySelect->addSelectionProxy(m_groupView->selectionModel()); } void MainWindow::initFileOpen(bool nofile_) { MARK; slotInit(); // check to see if most recent file should be opened bool happyStart = false; if(!nofile_ && Config::reopenLastFile()) { // Config::lastOpenFile() is the full URL, protocol included QUrl lastFile(Config::lastOpenFile()); // empty string is actually ok, it gets handled if(!lastFile.isEmpty() && lastFile.isValid()) { slotFileOpen(lastFile); happyStart = true; } } if(!happyStart) { // the document is created with an initial book collection, continue with that Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); m_fileSave->setEnabled(false); slotEnableOpenedActions(); slotEnableModifiedActions(false); slotEntryCount(); // tell the entry views and models that there are no images to load m_detailedView->slotRefreshImages(); } // show welcome text, even when opening an existing collection const int type = Kernel::self()->collectionType(); QString welcomeFile = DataFileRegistry::self()->locate(QStringLiteral("welcome.html")); QString text = FileHandler::readTextFile(QUrl::fromLocalFile(welcomeFile)); text.replace(QLatin1String("$FGCOLOR$"), Config::templateTextColor(type).name()); text.replace(QLatin1String("$BGCOLOR$"), Config::templateBaseColor(type).name()); text.replace(QLatin1String("$COLOR1$"), Config::templateHighlightedTextColor(type).name()); text.replace(QLatin1String("$COLOR2$"), Config::templateHighlightedBaseColor(type).name()); text.replace(QLatin1String("$IMGDIR$"), QUrl::fromLocalFile(ImageFactory::tempDir()).url()); text.replace(QLatin1String("$BANNER$"), i18n("Welcome to the Tellico Collection Manager")); text.replace(QLatin1String("$WELCOMETEXT$"), i18n("

Tellico is a tool for managing collections of books, " "videos, music, and whatever else you want to catalog.

" "

New entries can be added to your collection by " "entering data manually or by " "downloading data from " "various Internet sources.

")); m_entryView->showText(text); m_initialized = true; } // These are general options. // The options that can be changed in the "Configuration..." dialog // are taken care of by the ConfigDialog object. void MainWindow::saveOptions() { KConfigGroup config(KSharedConfig::openConfig(), "Main Window Options"); saveMainWindowSettings(config); config.writeEntry(QStringLiteral("Central Dock State"), m_dummyWindow->saveState()); Config::setShowEditWidget(m_toggleEntryEditor->isChecked()); // check any single dock widget, they all get locked together Config::setLockLayout(m_groupViewDock->isLocked()); KConfigGroup filesConfig(KSharedConfig::openConfig(), "Recent Files"); m_fileOpenRecent->saveEntries(filesConfig); if(!isNewDocument()) { Config::setLastOpenFile(Data::Document::self()->URL().url()); } Config::setViewWidget(m_viewStack->currentWidget()); // historical reasons // sorting by count was faked by sorting by phantom second column const int sortColumn = m_groupView->sortRole() == RowCountRole ? 1 : 0; Config::setGroupViewSortColumn(sortColumn); // ok to use SortColumn key, save semantics Config::setGroupViewSortAscending(m_groupView->sortOrder() == Qt::AscendingOrder); if(m_loanView) { const int sortColumn = m_loanView->sortRole() == RowCountRole ? 1 : 0; Config::setLoanViewSortAscending(sortColumn); // ok to use SortColumn key, save semantics Config::setLoanViewSortAscending(m_loanView->sortOrder() == Qt::AscendingOrder); } if(m_filterView) { const int sortColumn = m_filterView->sortRole() == RowCountRole ? 1 : 0; Config::setFilterViewSortAscending(sortColumn); // ok to use SortColumn key, save semantics Config::setFilterViewSortAscending(m_filterView->sortOrder() == Qt::AscendingOrder); } // this is used in the EntryEditDialog constructor, too KConfigGroup editDialogConfig(KSharedConfig::openConfig(), "Edit Dialog Options"); KWindowConfig::saveWindowSize(m_editDialog->windowHandle(), editDialogConfig); saveCollectionOptions(Data::Document::self()->collection()); Config::self()->save(); } void MainWindow::readCollectionOptions(Tellico::Data::CollPtr coll_) { if(!coll_) { myDebug() << "Bad, no collection in MainWindow::readCollectionOptions()"; return; } const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_)); KConfigGroup group(KSharedConfig::openConfig(), configGroup); QString defaultGroup = coll_->defaultGroupField(); QString entryGroup; if(coll_->type() != Data::Collection::Base) { entryGroup = group.readEntry("Group By", defaultGroup); } else { QUrl url = Kernel::self()->URL(); for(int i = 0; i < Config::maxCustomURLSettings(); ++i) { QUrl u(group.readEntry(QStringLiteral("URL_%1").arg(i))); if(url == u) { entryGroup = group.readEntry(QStringLiteral("Group By_%1").arg(i), defaultGroup); break; } } // fall back to old setting if(entryGroup.isEmpty()) { entryGroup = group.readEntry("Group By", defaultGroup); } } if(entryGroup.isEmpty() || (!coll_->entryGroups().contains(entryGroup) && entryGroup != Data::Collection::s_peopleGroupName)) { entryGroup = defaultGroup; } m_groupView->setGroupField(entryGroup); QString entryXSLTFile = Config::templateName(coll_->type()); if(entryXSLTFile.isEmpty()) { entryXSLTFile = QStringLiteral("Fancy"); // should never happen, but just in case } m_entryView->setXSLTFile(entryXSLTFile + QLatin1String(".xsl")); // make sure the right combo element is selected slotUpdateCollectionToolBar(coll_); } void MainWindow::saveCollectionOptions(Tellico::Data::CollPtr coll_) { // don't save initial collection options, or empty collections if(!coll_ || coll_->entryCount() == 0 || isNewDocument()) { return; } int configIndex = -1; QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_)); KConfigGroup config(KSharedConfig::openConfig(), configGroup); QString groupName; if(m_entryGrouping->currentItem() > -1 && static_cast(coll_->entryGroups().count()) > m_entryGrouping->currentItem()) { if(m_entryGrouping->currentText() == (QLatin1Char('<') + i18n("People") + QLatin1Char('>'))) { groupName = Data::Collection::s_peopleGroupName; } else { groupName = Kernel::self()->fieldNameByTitle(m_entryGrouping->currentText()); } if(coll_->type() != Data::Collection::Base) { config.writeEntry("Group By", groupName); } } if(coll_->type() == Data::Collection::Base) { // all of this is to have custom settings on a per file basis QUrl url = Kernel::self()->URL(); QList urls = QList() << url; QStringList groupBys = QStringList() << groupName; for(int i = 0; i < Config::maxCustomURLSettings(); ++i) { QUrl u = config.readEntry(QStringLiteral("URL_%1").arg(i), QUrl()); QString g = config.readEntry(QStringLiteral("Group By_%1").arg(i), QString()); if(!u.isEmpty() && url != u) { urls.append(u); groupBys.append(g); } else if(!u.isEmpty()) { configIndex = i; } } int limit = qMin(urls.count(), Config::maxCustomURLSettings()); for(int i = 0; i < limit; ++i) { config.writeEntry(QStringLiteral("URL_%1").arg(i), urls[i].url()); config.writeEntry(QStringLiteral("Group By_%1").arg(i), groupBys[i]); } } m_detailedView->saveConfig(coll_, configIndex); } void MainWindow::readOptions() { KConfigGroup mainWindowConfig(KSharedConfig::openConfig(), "Main Window Options"); applyMainWindowSettings(mainWindowConfig); m_dummyWindow->restoreState(mainWindowConfig.readEntry(QStringLiteral("Central Dock State"), QByteArray())); m_viewStack->setCurrentWidget(Config::viewWidget()); m_iconView->setMaxAllowedIconWidth(Config::maxIconSize()); connect(toolBar(QStringLiteral("collectionToolBar")), &QToolBar::iconSizeChanged, this, &MainWindow::slotUpdateToolbarIcons); // initialize the recent file list KConfigGroup filesConfig(KSharedConfig::openConfig(), "Recent Files"); m_fileOpenRecent->loadEntries(filesConfig); // sort by count if column = 1 int sortRole = Config::groupViewSortColumn() == 0 ? static_cast(Qt::DisplayRole) : static_cast(RowCountRole); Qt::SortOrder sortOrder = Config::groupViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder; m_groupView->setSorting(sortOrder, sortRole); BibtexHandler::s_quoteStyle = Config::useBraces() ? BibtexHandler::BRACES : BibtexHandler::QUOTES; // Don't read any options for the edit dialog here, since it's not yet initialized. // Put them in init() } bool MainWindow::querySaveModified() { bool completed = true; if(Data::Document::self()->isModified()) { QString str = i18n("The current file has been modified.\n" "Do you want to save it?"); int want_save = KMessageBox::warningYesNoCancel(this, str, i18n("Unsaved Changes"), KStandardGuiItem::save(), KStandardGuiItem::discard()); switch(want_save) { case KMessageBox::Yes: completed = fileSave(); break; case KMessageBox::No: Data::Document::self()->setModified(false); completed = true; break; case KMessageBox::Cancel: default: completed = false; break; } } return completed; } bool MainWindow::queryClose() { // in case we're still loading the images, cancel that Data::Document::self()->cancelImageWriting(); const bool willClose = m_editDialog->queryModified() && querySaveModified(); if (willClose) { ImageFactory::clean(true); saveOptions(); } return willClose; } void MainWindow::slotFileNew(int type_) { slotStatusMsg(i18n("Creating new document...")); // close the fields dialog slotHideCollectionFieldsDialog(); if(m_editDialog->queryModified() && querySaveModified()) { // remove filter and loan tabs, they'll get re-added if needed if(m_filterView) { m_viewTabs->removeTab(m_viewTabs->indexOf(m_filterView)); Controller::self()->removeObserver(m_filterView); delete m_filterView; m_filterView = nullptr; } if(m_loanView) { m_viewTabs->removeTab(m_viewTabs->indexOf(m_loanView)); Controller::self()->removeObserver(m_loanView); delete m_loanView; m_loanView = nullptr; } m_viewTabs->setTabBarHidden(true); Data::Document::self()->newDocument(type_); Kernel::self()->resetHistory(); m_fileOpenRecent->setCurrentItem(-1); slotEnableOpenedActions(); slotEnableModifiedActions(false); m_newDocument = true; ImageFactory::clean(false); } StatusBar::self()->clearStatus(); } void MainWindow::slotFileOpen() { slotStatusMsg(i18n("Opening file...")); if(m_editDialog->queryModified() && querySaveModified()) { QString filter = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)"); filter += QLatin1String(";;"); filter += i18n("XML Files") + QLatin1String(" (*.xml)"); filter += QLatin1String(";;"); filter += i18n("All Files") + QLatin1String(" (*)"); // keyword 'open' QString fileClass; const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///open")), fileClass); QUrl url = QFileDialog::getOpenFileUrl(this, i18n("Open File"), startUrl, filter); if(!url.isEmpty() && url.isValid()) { slotFileOpen(url); if(url.isLocalFile()) { KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } } } StatusBar::self()->clearStatus(); } void MainWindow::slotFileOpen(const QUrl& url_) { slotStatusMsg(i18n("Opening file...")); // close the fields dialog slotHideCollectionFieldsDialog(); // there seems to be a race condition at start between slotInit() and initFileOpen() // which means the edit dialog might not have been created yet if((!m_editDialog || m_editDialog->queryModified()) && querySaveModified()) { if(openURL(url_)) { m_fileOpenRecent->addUrl(url_); m_fileOpenRecent->setCurrentItem(-1); } } StatusBar::self()->clearStatus(); } void MainWindow::slotFileOpenRecent(const QUrl& url_) { slotStatusMsg(i18n("Opening file...")); // close the fields dialog slotHideCollectionFieldsDialog(); if(m_editDialog->queryModified() && querySaveModified()) { if(!openURL(url_)) { m_fileOpenRecent->removeUrl(url_); m_fileOpenRecent->setCurrentItem(-1); } } else { // the QAction shouldn't be checked now m_fileOpenRecent->setCurrentItem(-1); } StatusBar::self()->clearStatus(); } void MainWindow::openFile(const QString& file_) { QUrl url(file_); if(!url.isEmpty() && url.isValid()) { slotFileOpen(url); } } bool MainWindow::openURL(const QUrl& url_) { MARK; // try to open document GUI::CursorSaver cs(Qt::WaitCursor); bool success = Data::Document::self()->openDocument(url_); if(success) { Kernel::self()->resetHistory(); m_quickFilter->clear(); slotEnableOpenedActions(); m_newDocument = false; slotEnableModifiedActions(Data::Document::self()->isModified()); // doc might add some stuff } else if(!m_initialized) { // special case on startup when openURL() is called with a command line argument // and that URL can't be opened. The window still needs to be initialized // the doc object is created with an initial book collection, continue with that Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); m_fileSave->setEnabled(false); slotEnableOpenedActions(); slotEnableModifiedActions(false); slotEntryCount(); } // slotFileOpen(URL) gets called when opening files on the command line // so go ahead and make sure m_initialized is set. m_initialized = true; // remove filter and loan tabs, they'll get re-added if needed if(m_filterView && m_filterView->isEmpty()) { m_viewTabs->removeTab(m_viewTabs->indexOf(m_filterView)); Controller::self()->removeObserver(m_filterView); delete m_filterView; m_filterView = nullptr; } if(m_loanView && m_loanView->isEmpty()) { m_viewTabs->removeTab(m_viewTabs->indexOf(m_loanView)); Controller::self()->removeObserver(m_loanView); delete m_loanView; m_loanView = nullptr; } Controller::self()->hideTabs(); // does conditional check return success; } void MainWindow::slotFileSave() { fileSave(); } bool MainWindow::fileSave() { if(!m_editDialog->queryModified()) { return false; } slotStatusMsg(i18n("Saving file...")); bool ret = true; if(isNewDocument()) { ret = fileSaveAs(); } else { // special check: if there are more than 200 images AND the "Write Images In File" config key // is set, then warn user that performance may suffer, and write result if(Config::imageLocation() == Config::ImagesInFile && Config::askWriteImagesInFile() && Data::Document::self()->imageCount() > MAX_IMAGES_WARN_PERFORMANCE) { QString msg = i18n("

You are saving a file with many images, which causes Tellico to " "slow down significantly. Do you want to save the images separately in " "Tellico's data directory to improve performance?

Your choice can " "always be changed in the configuration dialog.

"); KGuiItem yes(i18n("Save Images Separately")); KGuiItem no(i18n("Save Images in File")); int res = KMessageBox::warningYesNo(this, msg, QString() /* caption */, yes, no); if(res == KMessageBox::No) { Config::setImageLocation(Config::ImagesInAppDir); } Config::setAskWriteImagesInFile(false); } GUI::CursorSaver cs(Qt::WaitCursor); if(Data::Document::self()->saveDocument(Data::Document::self()->URL())) { Kernel::self()->resetHistory(); m_newDocument = false; updateCaption(false); m_fileSave->setEnabled(false); // TODO: call a method of the model instead of the view here m_detailedView->resetEntryStatus(); } else { ret = false; } } StatusBar::self()->clearStatus(); return ret; } void MainWindow::slotFileSaveAs() { fileSaveAs(); } bool MainWindow::fileSaveAs() { if(!m_editDialog->queryModified()) { return false; } slotStatusMsg(i18n("Saving file with a new filename...")); QString filter = i18n("Tellico Files") + QLatin1String(" (*.tc *.bc)"); filter += QLatin1String(";;"); filter += i18n("All Files") + QLatin1String(" (*)"); // keyword 'open' QString fileClass; const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///open")), fileClass); const QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Save As"), startUrl, filter); if(url.isEmpty()) { StatusBar::self()->clearStatus(); return false; } if(url.isLocalFile()) { KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } bool ret = true; if(url.isValid()) { GUI::CursorSaver cs(Qt::WaitCursor); m_savingImageLocationChange = true; // Overwriting an existing file was already confirmed in QFileDialog::getSaveFileUrl() if(Data::Document::self()->saveDocument(url, true /* force */)) { Kernel::self()->resetHistory(); KRecentDocument::add(url); m_fileOpenRecent->addUrl(url); updateCaption(false); m_newDocument = false; m_fileSave->setEnabled(false); m_detailedView->resetEntryStatus(); } else { ret = false; } m_savingImageLocationChange = false; } StatusBar::self()->clearStatus(); return ret; } void MainWindow::slotFilePrint() { slotStatusMsg(i18n("Printing...")); bool printGrouped = Config::printGrouped(); bool printHeaders = Config::printFieldHeaders(); int imageWidth = Config::maxImageWidth(); int imageHeight = Config::maxImageHeight(); // If the collection is being filtered, warn the user if(m_detailedView->filter()) { QString str = i18n("The collection is currently being filtered to show a limited subset of " "the entries. Only the visible entries will be printed. Continue?"); int ret = KMessageBox::warningContinueCancel(this, str, QString(), KStandardGuiItem::print(), KStandardGuiItem::cancel(), QStringLiteral("WarnPrintVisible")); if(ret == KMessageBox::Cancel) { StatusBar::self()->clearStatus(); return; } } GUI::CursorSaver cs(Qt::WaitCursor); Export::HTMLExporter exporter(Data::Document::self()->collection()); // only print visible entries exporter.setEntries(m_detailedView->visibleEntries()); exporter.setXSLTFile(QStringLiteral("tellico-printing.xsl")); exporter.setPrintHeaders(printHeaders); exporter.setPrintGrouped(printGrouped); exporter.setGroupBy(Controller::self()->expandedGroupBy()); if(!printGrouped) { // the sort titles are only used if the entries are not grouped exporter.setSortTitles(Controller::self()->sortTitles()); } exporter.setColumns(m_detailedView->visibleColumns()); exporter.setMaxImageSize(imageWidth, imageHeight); slotStatusMsg(i18n("Processing document...")); if(Config::printFormatted()) { exporter.setOptions(Export::ExportUTF8 | Export::ExportFormatted); } else { exporter.setOptions(Export::ExportUTF8); } QString html = exporter.text(); if(html.isEmpty()) { XSLTError(); StatusBar::self()->clearStatus(); return; } // don't have busy cursor when showing the print dialog cs.restore(); // myDebug() << html; slotStatusMsg(i18n("Printing...")); doPrint(html); StatusBar::self()->clearStatus(); } void MainWindow::slotFileQuit() { slotStatusMsg(i18n("Exiting...")); close(); // will call queryClose() StatusBar::self()->clearStatus(); } void MainWindow::slotEditCut() { activateEditSlot("cut()"); } void MainWindow::slotEditCopy() { activateEditSlot("copy()"); } void MainWindow::slotEditPaste() { activateEditSlot("paste()"); } void MainWindow::activateEditSlot(const char* slot_) { // the edit widget is the only one that copies, cuts, and pastes QWidget* w; if(m_editDialog->isVisible()) { w = m_editDialog->focusWidget(); } else { w = qApp->focusWidget(); } if(w && w->isVisible()) { const QMetaObject* meta = w->metaObject(); const int idx = meta->indexOfSlot(slot_); if(idx > -1) { //myDebug() << "MainWindow invoking" << meta->method(idx).signature(); meta->method(idx).invoke(w, Qt::DirectConnection); } } } void MainWindow::slotEditSelectAll() { m_detailedView->selectAllVisible(); } void MainWindow::slotEditDeselect() { Controller::self()->slotUpdateSelection(Data::EntryList()); } void MainWindow::slotToggleEntryEditor() { if(m_toggleEntryEditor->isChecked()) { m_editDialog->show(); } else { m_editDialog->hide(); } } void MainWindow::slotShowConfigDialog() { if(!m_configDlg) { m_configDlg = new ConfigDialog(this); connect(m_configDlg, &ConfigDialog::signalConfigChanged, this, &MainWindow::slotHandleConfigChange); connect(m_configDlg, &QDialog::finished, this, &MainWindow::slotHideConfigDialog); } else { KWindowSystem::activateWindow(m_configDlg->winId()); } m_configDlg->show(); } void MainWindow::slotHideConfigDialog() { if(m_configDlg) { m_configDlg->hide(); m_configDlg->deleteLater(); m_configDlg = nullptr; } } void MainWindow::slotShowTipOfDay(bool force_/*=true*/) { KTipDialog::showTip(this, QStringLiteral("tellico/tellico.tips"), force_); } void MainWindow::slotStatusMsg(const QString& text_) { m_statusBar->setStatus(text_); } void MainWindow::slotClearStatus() { StatusBar::self()->clearStatus(); } void MainWindow::slotEntryCount() { Data::CollPtr coll = Data::Document::self()->collection(); if(!coll) { return; } int count = coll->entryCount(); QString text = i18n("Total entries: %1", count); int selectCount = Controller::self()->selectedEntries().count(); int filterCount = m_detailedView->visibleItems(); // if more than one book is selected, add the number of selected books if(filterCount < count && selectCount > 1) { text += QLatin1Char(' '); text += i18n("(%1 filtered; %2 selected)", filterCount, selectCount); } else if(filterCount < count) { text += QLatin1Char(' '); text += i18n("(%1 filtered)", filterCount); } else if(selectCount > 1) { text += QLatin1Char(' '); text += i18n("(%1 selected)", selectCount); } m_statusBar->setCount(text); } void MainWindow::slotEnableOpenedActions() { slotUpdateToolbarIcons(); updateCollectionActions(); // close the filter dialog when a new collection is opened slotHideFilterDialog(); slotStringMacroDialogFinished(); } void MainWindow::slotEnableModifiedActions(bool modified_ /*= true*/) { updateCaption(modified_); updateCollectionActions(); m_fileSave->setEnabled(modified_); } void MainWindow::slotHandleConfigChange() { const int imageLocation = Config::imageLocation(); const bool autoCapitalize = Config::autoCapitalization(); const bool autoFormat = Config::autoFormat(); const QStringList articles = Config::articleList(); const QStringList nocaps = Config::noCapitalizationList(); const QStringList suffixes = Config::nameSuffixList(); const QStringList prefixes = Config::surnamePrefixList(); m_configDlg->saveConfiguration(); // only modified if there are entries and image location is changed if(imageLocation != Config::imageLocation() && !Data::Document::self()->isEmpty()) { slotImageLocationChanged(); } if(autoCapitalize != Config::autoCapitalization() || autoFormat != Config::autoFormat() || articles != Config::articleList() || nocaps != Config::noCapitalizationList() || suffixes != Config::nameSuffixList() || prefixes != Config::surnamePrefixList()) { // invalidate all groups Data::Document::self()->collection()->invalidateGroups(); // refreshing the title causes the group view to refresh Controller::self()->slotRefreshField(Data::Document::self()->collection()->fieldByName(QStringLiteral("title"))); } QString entryXSLTFile = Config::templateName(Kernel::self()->collectionType()); m_entryView->setXSLTFile(entryXSLTFile + QLatin1String(".xsl")); } void MainWindow::slotUpdateCollectionToolBar(Tellico::Data::CollPtr coll_) { if(!coll_) { myWarning() << "no collection pointer!"; return; } QString current = m_groupView->groupBy(); if(current.isEmpty() || !coll_->entryGroups().contains(current)) { current = coll_->defaultGroupField(); } const QStringList groups = coll_->entryGroups(); if(groups.isEmpty()) { m_entryGrouping->clear(); return; } QMap groupMap; // use a map so they get sorted foreach(const QString& groupName, groups) { // special case for people "pseudo-group" if(groupName == Data::Collection::s_peopleGroupName) { groupMap.insert(groupName, QLatin1Char('<') + i18n("People") + QLatin1Char('>')); } else { groupMap.insert(groupName, coll_->fieldTitleByName(groupName)); } } const QStringList titles = groupMap.values(); if(titles == m_entryGrouping->items()) { // no need to update anything return; } const QStringList names = groupMap.keys(); int index = names.indexOf(current); if(index == -1) { current = names[0]; index = 0; } m_entryGrouping->setItems(titles); m_entryGrouping->setCurrentItem(index); // in case the current grouping field get modified to be non-grouping... m_groupView->setGroupField(current); // don't call slotChangeGrouping() since it adds an undo item // TODO::I have no idea how to get the combobox to update its size // this is the hackiest of hacks, taken from KXmlGuiWindow::saveNewToolbarConfig() // the window flickers as toolbar resizes, unavoidable? // crashes if removeCLient//addClient is called here, need to do later in event loop QTimer::singleShot(0, this, &MainWindow::guiFactoryReset); } void MainWindow::slotChangeGrouping() { const QString title = m_entryGrouping->currentText(); QString groupName = Kernel::self()->fieldNameByTitle(title); if(groupName.isEmpty()) { if(title == (QLatin1Char('<') + i18n("People") + QLatin1Char('>'))) { groupName = Data::Collection::s_peopleGroupName; } else { groupName = Data::Document::self()->collection()->defaultGroupField(); } } m_groupView->setGroupField(groupName); m_viewTabs->setCurrentWidget(m_groupView); } void MainWindow::slotShowReportDialog() { if(!m_reportDlg) { m_reportDlg = new ReportDialog(this); connect(m_reportDlg, &QDialog::finished, this, &MainWindow::slotHideReportDialog); } else { KWindowSystem::activateWindow(m_reportDlg->winId()); } m_reportDlg->show(); } void MainWindow::slotHideReportDialog() { if(m_reportDlg) { m_reportDlg->hide(); m_reportDlg->deleteLater(); m_reportDlg = nullptr; } } void MainWindow::doPrint(const QString& html_) { KHTMLPart w; // KHTMLPart printing was broken in KDE until KHTML 5.16 // see https://git.reviewboard.kde.org/r/125681/ const QString version = w.componentData().version(); const uint major = version.section(QLatin1Char('.'), 0, 0).toUInt(); const uint minor = version.section(QLatin1Char('.'), 1, 1).toUInt(); if(major == 5 && minor < 16) { myWarning() << "Printing is broken for KDE Frameworks < 5.16. Please upgrade"; return; } w.setJScriptEnabled(false); w.setJavaEnabled(false); w.setMetaRefreshEnabled(false); w.setPluginsEnabled(false); w.begin(Data::Document::self()->URL()); w.write(html_); w.end(); // the problem with doing my own layout is that the text gets truncated, both at the // top and at the bottom. Even adding the overlap parameter, there were problems. // KHTMLView takes care of that with a truncatedAt() parameter, but that's hidden in // the khtml::render_root class. So for now, just use the KHTMLView::print() method. w.view()->print(); } void MainWindow::XSLTError() { QString str = i18n("Tellico encountered an error in XSLT processing.") + QLatin1Char('\n'); str += i18n("Please check your installation."); Kernel::self()->sorry(str); } void MainWindow::slotShowFilterDialog() { if(!m_filterDlg) { m_filterDlg = new FilterDialog(FilterDialog::CreateFilter, this); // allow saving m_quickFilter->setEnabled(false); connect(m_filterDlg, &FilterDialog::signalCollectionModified, Data::Document::self(), &Data::Document::slotSetModified); connect(m_filterDlg, &FilterDialog::signalUpdateFilter, this, &MainWindow::slotUpdateFilter); connect(m_filterDlg, &QDialog::finished, this, &MainWindow::slotHideFilterDialog); } else { KWindowSystem::activateWindow(m_filterDlg->winId()); } m_filterDlg->setFilter(m_detailedView->filter()); m_filterDlg->show(); } void MainWindow::slotHideFilterDialog() { // m_quickFilter->blockSignals(false); m_quickFilter->setEnabled(true); if(m_filterDlg) { m_filterDlg->hide(); m_filterDlg->deleteLater(); m_filterDlg = nullptr; } } void MainWindow::slotQueueFilter() { if(m_dontQueueFilter) { return; } m_queuedFilters++; QTimer::singleShot(200, this, &MainWindow::slotCheckFilterQueue); } void MainWindow::slotCheckFilterQueue() { m_queuedFilters--; if(m_queuedFilters > 0) { return; } setFilter(m_quickFilter->text()); } void MainWindow::slotUpdateFilter(FilterPtr filter_) { // Can't just block signals because clear button won't show then m_dontQueueFilter = true; m_quickFilter->setText(QStringLiteral(" ")); // To be able to clear custom filter Controller::self()->slotUpdateFilter(filter_); m_dontQueueFilter = false; } void MainWindow::setFilter(const QString& text_) { QString text = text_.trimmed(); FilterPtr filter; if(!text.isEmpty()) { filter = new Filter(Filter::MatchAll); QString fieldName; // empty field name means match on any field // if the text contains '=' assume it's a field name or title if(text.indexOf(QLatin1Char('=')) > -1) { fieldName = text.section(QLatin1Char('='), 0, 0).trimmed(); text = text.section(QLatin1Char('='), 1).trimmed(); // check that the field name might be a title if(!Data::Document::self()->collection()->hasField(fieldName)) { fieldName = Data::Document::self()->collection()->fieldNameByTitle(fieldName); } } // if the text contains any non-word characters, assume it's a regexp // but \W in qt is letter, number, or '_', I want to be a bit less strict QRegExp rx(QLatin1String("[^\\w\\s-']")); if(rx.indexIn(text) == -1) { // split by whitespace, and add rules for each word const QStringList tokens = text.split(QRegExp(QLatin1String("\\s"))); foreach(const QString& token, tokens) { // an empty field string means check every field filter->append(new FilterRule(fieldName, token, FilterRule::FuncContains)); } } else { // if it isn't valid, hold off on applying the filter QRegExp tx(text); if(!tx.isValid()) { text = QRegExp::escape(text); tx.setPattern(text); } if(!tx.isValid()) { myDebug() << "invalid regexp:" << text; return; } filter->append(new FilterRule(fieldName, text, FilterRule::FuncRegExp)); } // also want to update the line edit in case the filter was set by DBUS if(m_quickFilter->text() != text_) { m_quickFilter->setText(text_); } } // only update filter if one exists or did exist if(filter || m_detailedView->filter()) { Controller::self()->slotUpdateFilter(filter); } } void MainWindow::slotShowCollectionFieldsDialog() { if(!m_collFieldsDlg) { m_collFieldsDlg = new CollectionFieldsDialog(Data::Document::self()->collection(), this); connect(m_collFieldsDlg, &QDialog::finished, this, &MainWindow::slotHideCollectionFieldsDialog); } else { KWindowSystem::activateWindow(m_collFieldsDlg->winId()); } m_collFieldsDlg->show(); } void MainWindow::slotHideCollectionFieldsDialog() { if(m_collFieldsDlg) { m_collFieldsDlg->hide(); m_collFieldsDlg->deleteLater(); m_collFieldsDlg = nullptr; } } void MainWindow::slotFileImport(int format_) { slotStatusMsg(i18n("Importing data...")); m_quickFilter->clear(); Import::Format format = static_cast(format_); bool checkURL = true; QUrl url; switch(ImportDialog::importTarget(format)) { case Import::File: { QString fileClass; const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///import")), fileClass); url = QFileDialog::getOpenFileUrl(this, i18n("Import File"), startUrl, ImportDialog::fileFilter(format)); KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } break; case Import::Dir: // TODO: allow remote audiofile importing { const QString fileClass(QStringLiteral("ImportDir")); QString dirName = ImportDialog::startDir(format); if(dirName.isEmpty()) { dirName = KRecentDirs::dir(fileClass); } url = QUrl::fromLocalFile(QFileDialog::getExistingDirectory(this, i18n("Import Directory"), dirName)); KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } break; case Import::None: default: checkURL = false; break; } if(checkURL) { bool ok = !url.isEmpty() && url.isValid() && QFile::exists(url.toLocalFile()); if(!ok) { StatusBar::self()->clearStatus(); return; } } importFile(format, QList() << url); StatusBar::self()->clearStatus(); } void MainWindow::slotFileExport(int format_) { slotStatusMsg(i18n("Exporting data...")); Export::Format format = static_cast(format_); ExportDialog dlg(format, Data::Document::self()->collection(), this); if(dlg.exec() == QDialog::Rejected) { StatusBar::self()->clearStatus(); return; } switch(ExportDialog::exportTarget(format)) { case Export::None: dlg.exportURL(); break; case Export::Dir: myDebug() << "ExportDir not implemented!"; break; case Export::File: { QString fileClass; const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///export")), fileClass); QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Export As"), startUrl, dlg.fileFilter()); if(url.isEmpty()) { StatusBar::self()->clearStatus(); return; } if(url.isValid()) { if(url.isLocalFile()) { KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } GUI::CursorSaver cs(Qt::WaitCursor); dlg.exportURL(url); } } break; } StatusBar::self()->clearStatus(); } void MainWindow::slotShowStringMacroDialog() { if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) { return; } if(!m_stringMacroDlg) { const Data::BibtexCollection* c = static_cast(Data::Document::self()->collection().data()); m_stringMacroDlg = new StringMapDialog(c->macroList(), this, false); m_stringMacroDlg->setWindowTitle(i18n("String Macros")); m_stringMacroDlg->setLabels(i18n("Macro"), i18n("String")); connect(m_stringMacroDlg, &QDialog::finished, this, &MainWindow::slotStringMacroDialogFinished); } else { KWindowSystem::activateWindow(m_stringMacroDlg->winId()); } m_stringMacroDlg->show(); } void MainWindow::slotStringMacroDialogFinished(int result_) { // no point in checking if collection is bibtex, as dialog would never have been created if(!m_stringMacroDlg) { return; } if(result_ == QDialog::Accepted) { static_cast(Data::Document::self()->collection().data())->setMacroList(m_stringMacroDlg->stringMap()); Data::Document::self()->setModified(true); } m_stringMacroDlg->hide(); m_stringMacroDlg->deleteLater(); m_stringMacroDlg = nullptr; } void MainWindow::slotShowBibtexKeyDialog() { if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) { return; } if(!m_bibtexKeyDlg) { m_bibtexKeyDlg = new BibtexKeyDialog(Data::Document::self()->collection(), this); connect(m_bibtexKeyDlg, &QDialog::finished, this, &MainWindow::slotHideBibtexKeyDialog); connect(m_bibtexKeyDlg, &BibtexKeyDialog::signalUpdateFilter, this, &MainWindow::slotUpdateFilter); } else { KWindowSystem::activateWindow(m_bibtexKeyDlg->winId()); } m_bibtexKeyDlg->show(); } void MainWindow::slotHideBibtexKeyDialog() { if(m_bibtexKeyDlg) { m_bibtexKeyDlg->deleteLater(); m_bibtexKeyDlg = nullptr; } } void MainWindow::slotNewEntry() { m_toggleEntryEditor->setChecked(true); slotToggleEntryEditor(); m_editDialog->slotHandleNew(); } void MainWindow::slotEditDialogFinished() { m_toggleEntryEditor->setChecked(false); } void MainWindow::slotShowEntryEditor() { m_toggleEntryEditor->setChecked(true); m_editDialog->show(); KWindowSystem::activateWindow(m_editDialog->winId()); } void MainWindow::slotConvertToBibliography() { // only book collections can be converted to bibtex Data::CollPtr coll = Data::Document::self()->collection(); if(!coll || coll->type() != Data::Collection::Book) { return; } GUI::CursorSaver cs; // need to make sure all images are transferred Data::Document::self()->loadAllImagesNow(); Data::CollPtr newColl = Data::BibtexCollection::convertBookCollection(coll); if(newColl) { m_newDocument = true; Kernel::self()->replaceCollection(newColl); m_fileOpenRecent->setCurrentItem(-1); slotUpdateToolbarIcons(); updateCollectionActions(); } else { myWarning() << "ERROR: no bibliography created!"; } } void MainWindow::slotCiteEntry(int action_) { StatusBar::self()->setStatus(i18n("Creating citations...")); Cite::ActionManager* man = Cite::ActionManager::self(); man->cite(static_cast(action_), Controller::self()->selectedEntries()); if(man->hasError()) { Kernel::self()->sorry(man->errorString()); } StatusBar::self()->clearStatus(); } void MainWindow::slotShowFetchDialog() { if(!m_fetchDlg) { m_fetchDlg = new FetchDialog(this); connect(m_fetchDlg, &QDialog::finished, this, &MainWindow::slotHideFetchDialog); connect(Controller::self(), &Controller::collectionAdded, m_fetchDlg, &FetchDialog::slotResetCollection); } else { KWindowSystem::activateWindow(m_fetchDlg->winId()); } m_fetchDlg->show(); } void MainWindow::slotHideFetchDialog() { if(m_fetchDlg) { m_fetchDlg->hide(); m_fetchDlg->deleteLater(); m_fetchDlg = nullptr; } } bool MainWindow::importFile(Tellico::Import::Format format_, const QUrl& url_, Tellico::Import::Action action_) { // try to open document GUI::CursorSaver cs(Qt::WaitCursor); bool failed = false; Data::CollPtr coll; if(!url_.isEmpty() && url_.isValid() && NetAccess::exists(url_, true, this)) { coll = ImportDialog::importURL(format_, url_); } else { Kernel::self()->sorry(i18n(errorLoad, url_.fileName())); failed = true; } if(!coll && !m_initialized) { // special case on startup when openURL() is called with a command line argument // and that URL can't be opened. The window still needs to be initialized // the doc object is created with an initial book collection, continue with that Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); m_fileSave->setEnabled(false); slotEnableOpenedActions(); slotEnableModifiedActions(false); slotEntryCount(); m_fileOpenRecent->setCurrentItem(-1); m_initialized = true; failed = true; } else if(coll) { // this is rather dumb, but I'm too lazy to find the bug // if the document isn't initialized, then Tellico crashes // since Document::replaceCollection() ends up calling lots of stuff that isn't initialized if(!m_initialized) { Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); m_initialized = true; } failed = !importCollection(coll, action_); } StatusBar::self()->clearStatus(); return !failed; // return true means success } bool MainWindow::exportCollection(Tellico::Export::Format format_, const QUrl& url_, bool filtered_) { if(!url_.isValid()) { myDebug() << "invalid URL:" << url_; return false; } GUI::CursorSaver cs; const Data::CollPtr coll = Data::Document::self()->collection(); if(!coll) { return false; } // only bibliographies can export to bibtex or bibtexml const bool isBibtex = (coll->type() == Data::Collection::Bibtex); if(!isBibtex && (format_ == Export::Bibtex || format_ == Export::Bibtexml)) { return false; } // only books and bibliographies can export to alexandria const bool isBook = (coll->type() == Data::Collection::Book); if(!isBibtex && !isBook && format_ == Export::Alexandria) { return false; } return ExportDialog::exportCollection(coll, filtered_ ? Controller::self()->visibleEntries() : coll->entries(), format_, url_); } bool MainWindow::showEntry(Data::ID id) { Data::EntryPtr entry = Data::Document::self()->collection()->entryById(id); if(entry) { m_entryView->showEntry(entry); } return entry; } void MainWindow::addFilterView() { if(m_filterView) { return; } m_filterView = new FilterView(m_viewTabs); Controller::self()->addObserver(m_filterView); m_viewTabs->insertTab(1, m_filterView, QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Filters")); m_filterView->setWhatsThis(i18n("The Filter View shows the entries which meet certain " "filter rules.")); connect(m_filterView, &FilterView::signalUpdateFilter, this, &MainWindow::slotUpdateFilter); // use the EntrySelectionModel as a proxy so when entries get selected in the filter view // the edit dialog and entry view are updated // TODO: consider using KSelectionProxyModel static_cast(m_iconView->selectionModel())->addSelectionProxy(m_filterView->selectionModel()); // sort by count if column = 1 int sortRole = Config::filterViewSortColumn() == 0 ? static_cast(Qt::DisplayRole) : static_cast(RowCountRole); Qt::SortOrder sortOrder = Config::filterViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder; m_filterView->setSorting(sortOrder, sortRole); } void MainWindow::addLoanView() { if(m_loanView) { return; } m_loanView = new LoanView(m_viewTabs); Controller::self()->addObserver(m_loanView); m_viewTabs->insertTab(2, m_loanView, QIcon::fromTheme(QStringLiteral("kaddressbook")), i18n("Loans")); m_loanView->setWhatsThis(i18n("The Loan View shows a list of all the people who " "have borrowed items from your collection.")); // use the EntrySelectionModel as a proxy so when entries get selected in the loan view // the edit dialog and entry view are updated // TODO: consider using KSelectionProxyModel static_cast(m_iconView->selectionModel())->addSelectionProxy(m_loanView->selectionModel()); // sort by count if column = 1 int sortRole = Config::loanViewSortColumn() == 0 ? static_cast(Qt::DisplayRole) : static_cast(RowCountRole); Qt::SortOrder sortOrder = Config::loanViewSortAscending() ? Qt::AscendingOrder : Qt::DescendingOrder; m_loanView->setSorting(sortOrder, sortRole); } void MainWindow::updateCaption(bool modified_) { QString caption; if(Data::Document::self()->collection()) { caption = Data::Document::self()->collection()->title(); } if(!m_newDocument) { if(!caption.isEmpty()) { caption += QLatin1String(" - "); } QUrl u = Data::Document::self()->URL(); if(u.isLocalFile()) { // for new files, the filename is set to Untitled in Data::Document if(u.fileName() == i18n(Tellico::untitledFilename)) { caption += u.fileName(); } else { caption += u.path(); } } else { caption += u.toDisplayString(); } } setCaption(caption, modified_); } void MainWindow::slotUpdateToolbarIcons() { // first change the icon for the menu item if(Kernel::self()->collectionType() == Data::Collection::Base) { m_newEntry->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); } else { m_newEntry->setIcon(QIcon(QLatin1String(":/icons/") + Kernel::self()->collectionTypeName())); } } void MainWindow::slotGroupLabelActivated() { // need entry grouping combo id foreach(QWidget* widget, m_entryGrouping->associatedWidgets()) { if(::qobject_cast(widget)) { QWidget* container = m_entryGrouping->requestWidget(widget); QComboBox* combo = ::qobject_cast(container); //krazy:exclude=qclasses if(combo) { combo->showPopup(); break; } } } } void MainWindow::slotFilterLabelActivated() { m_quickFilter->setFocus(); m_quickFilter->selectAll(); } void MainWindow::slotClearFilter() { m_quickFilter->clear(); slotQueueFilter(); } void MainWindow::slotRenameCollection() { Kernel::self()->renameCollection(); } void MainWindow::slotImageLocationMismatch() { // TODO: having a single image location mismatch should not be reason to completely save the whole document QTimer::singleShot(0, this, &MainWindow::slotImageLocationChanged); } void MainWindow::slotImageLocationChanged() { if(m_savingImageLocationChange) { return; } m_savingImageLocationChange = true; Data::Document::self()->slotSetModified(); KMessageBox::information(this, QLatin1String("") + i18n("Some images are not saved in the configured location. The current file " "must be saved and the images will be transferred to the new location.") + QLatin1String("")); fileSave(); m_savingImageLocationChange = false; } void MainWindow::updateCollectionActions() { if(!Data::Document::self()->collection()) { return; } stateChanged(QStringLiteral("collection_reset")); Data::Collection::Type type = Data::Document::self()->collection()->type(); stateChanged(QLatin1String("is_") + CollectionFactory::typeName(type)); Controller::self()->updateActions(); // special case when there are no available data sources if(m_fetchActions.isEmpty() && m_updateAll) { m_updateAll->setEnabled(false); } } void MainWindow::updateEntrySources() { unplugActionList(QStringLiteral("update_entry_actions")); foreach(QAction* action, m_fetchActions) { foreach(QWidget* widget, action->associatedWidgets()) { widget->removeAction(action); } m_updateMapper->removeMappings(action); } qDeleteAll(m_fetchActions); m_fetchActions.clear(); Fetch::FetcherVec vec = Fetch::Manager::self()->fetchers(Kernel::self()->collectionType()); foreach(Fetch::Fetcher::Ptr fetcher, vec) { QAction* action = new QAction(Fetch::Manager::fetcherIcon(fetcher), fetcher->source(), actionCollection()); action->setToolTip(i18n("Update entry data from %1", fetcher->source())); void (QAction::* triggeredBool)(bool) = &QAction::triggered; void (QSignalMapper::* mapVoid)() = &QSignalMapper::map; connect(action, triggeredBool, m_updateMapper, mapVoid); m_updateMapper->setMapping(action, fetcher->source()); m_fetchActions.append(action); } plugActionList(QStringLiteral("update_entry_actions"), m_fetchActions); } void MainWindow::importFile(Tellico::Import::Format format_, const QList& urls_) { QList urls = urls_; // update as DropHandler and Importer classes are updated if(urls_.count() > 1 && format_ != Import::Bibtex && format_ != Import::RIS && format_ != Import::CIW && format_ != Import::PDF) { QUrl u = urls_.front(); QString url = u.isLocalFile() ? u.path() : u.toDisplayString(); Kernel::self()->sorry(i18n("Tellico can only import one file of this type at a time. " "Only %1 will be imported.", url)); urls.clear(); urls += u; } ImportDialog dlg(format_, urls, this); if(dlg.exec() != QDialog::Accepted) { return; } // if edit dialog is saved ok and if replacing, then the doc is saved ok if(m_editDialog->queryModified() && (dlg.action() != Import::Replace || querySaveModified())) { GUI::CursorSaver cs(Qt::WaitCursor); Data::CollPtr coll = dlg.collection(); if(!coll) { if(!dlg.statusMessage().isEmpty()) { Kernel::self()->sorry(dlg.statusMessage()); } return; } importCollection(coll, dlg.action()); } } void MainWindow::importText(Tellico::Import::Format format_, const QString& text_) { if(text_.isEmpty()) { return; } Data::CollPtr coll = ImportDialog::importText(format_, text_); if(coll) { importCollection(coll, Import::Merge); } } bool MainWindow::importCollection(Tellico::Data::CollPtr coll_, Tellico::Import::Action action_) { bool failed = false; switch(action_) { case Import::Append: { // only append if match, but special case importing books into bibliographies Data::CollPtr c = Data::Document::self()->collection(); if(c->type() == coll_->type() || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) { Kernel::self()->appendCollection(coll_); slotEnableModifiedActions(true); } else { Kernel::self()->sorry(i18n(errorAppendType)); failed = true; } } break; case Import::Merge: { // only merge if match, but special case importing books into bibliographies Data::CollPtr c = Data::Document::self()->collection(); if(c->type() == coll_->type() || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) { Kernel::self()->mergeCollection(coll_); slotEnableModifiedActions(true); } else { Kernel::self()->sorry(i18n(errorMergeType)); failed = true; } } break; default: // replace Kernel::self()->replaceCollection(coll_); m_fileOpenRecent->setCurrentItem(-1); m_newDocument = true; slotEnableOpenedActions(); slotEnableModifiedActions(false); break; } // tell the entry views and models that there are no further images to load m_detailedView->slotRefreshImages(); return !failed; } void MainWindow::slotURLAction(const QUrl& url_) { Q_ASSERT(url_.scheme() == QLatin1String("tc")); QString actionName = url_.fileName(); QAction* action = this->action(actionName.toLatin1().constData()); if(action) { action->activate(QAction::Trigger); } else { myWarning() << "unknown action: " << actionName; } } bool MainWindow::eventFilter(QObject* obj_, QEvent* ev_) { if(ev_->type() == QEvent::KeyPress && obj_ == m_quickFilter) { switch(static_cast(ev_)->key()) { case Qt::Key_Escape: m_quickFilter->clear(); return true; } } return KXmlGuiWindow::eventFilter(obj_, ev_); } void MainWindow::slotToggleFullScreen() { Qt::WindowStates ws = windowState(); setWindowState((ws & Qt::WindowFullScreen) ? (ws & ~Qt::WindowFullScreen) : (ws | Qt::WindowFullScreen)); } void MainWindow::slotToggleMenuBarVisibility() { QMenuBar* mb = menuBar(); mb->isHidden() ? mb->show() : mb->hide(); } void MainWindow::slotToggleLayoutLock(bool lock_) { m_groupViewDock->setLocked(lock_); m_collectionViewDock->setLocked(lock_); } void MainWindow::slotResetLayout() { removeDockWidget(m_groupViewDock); addDockWidget(Qt::LeftDockWidgetArea, m_groupViewDock); m_groupViewDock->show(); m_dummyWindow->removeDockWidget(m_collectionViewDock); m_dummyWindow->addDockWidget(Qt::TopDockWidgetArea, m_collectionViewDock); m_collectionViewDock->show(); } void MainWindow::guiFactoryReset() { guiFactory()->removeClient(this); guiFactory()->reset(); guiFactory()->addClient(this); } diff --git a/src/tellicoui.rc b/src/tellicoui.rc index d2fd2320..2792add9 100644 --- a/src/tellicoui.rc +++ b/src/tellicoui.rc @@ -1,232 +1,233 @@ - + &File &New &Import + &Export &Collection &Update Entry &Bibliography &Show Views Main Toolbar Collection Toolbar diff --git a/src/translators/CMakeLists.txt b/src/translators/CMakeLists.txt index e93e9691..7d18cfe7 100644 --- a/src/translators/CMakeLists.txt +++ b/src/translators/CMakeLists.txt @@ -1,101 +1,102 @@ ########### next target ############### SET(translators_STAT_SRCS adsimporter.cpp alexandriaexporter.cpp alexandriaimporter.cpp amcimporter.cpp audiofileimporter.cpp bibtexexporter.cpp bibteximporter.cpp bibtexmlexporter.cpp bibtexmlimporter.cpp boardgamegeekimporter.cpp ciwimporter.cpp csvexporter.cpp csvimporter.cpp csvparser.cpp dataimporter.cpp deliciousimporter.cpp exporter.cpp filelistingimporter.cpp freedb_util.cpp freedbimporter.cpp gcstarexporter.cpp gcstarimporter.cpp goodreadsimporter.cpp griffithimporter.cpp grs1importer.cpp htmlexporter.cpp importer.cpp + librarythingimporter.cpp onixexporter.cpp pdfimporter.cpp referencerimporter.cpp risimporter.cpp tellico_xml.cpp tellicoimporter.cpp tellicoxmlexporter.cpp tellicoxmlhandler.cpp tellicozipexporter.cpp textimporter.cpp vinoxmlimporter.cpp xmlstatehandler.cpp xmphandler.cpp xsltexporter.cpp xslthandler.cpp xsltimporter.cpp ) add_library(translators STATIC ${translators_STAT_SRCS}) TARGET_LINK_LIBRARIES(translators collections tellicomodels core gui images utils ${TELLICO_BTPARSE_LIBS} ${TELLICO_CSV_LIBS} rtf2html-tellico ) TARGET_LINK_LIBRARIES(translators KF5::Archive KF5::JobWidgets KF5::Solid ${LIBXML2_LIBRARIES} ${LIBXSLT_LIBRARIES} ${LIBXSLT_EXSLT_LIBRARIES} ) IF( KF5Cddb_FOUND ) TARGET_LINK_LIBRARIES(translators KF5::Cddb) ELSEIF( Libkcddb_FOUND ) TARGET_LINK_LIBRARIES(translators KDE::Libkcddb::kcddb) ENDIF( KF5Cddb_FOUND) IF( Poppler_Qt5_FOUND ) TARGET_LINK_LIBRARIES(translators Poppler::Qt5) ENDIF( Poppler_Qt5_FOUND ) IF( KF5FileMetaData_FOUND ) TARGET_LINK_LIBRARIES(translators KF5::FileMetaData) ENDIF( KF5FileMetaData_FOUND ) IF( Exempi_FOUND ) TARGET_LINK_LIBRARIES(translators ${Exempi_LIBRARIES}) ENDIF( Exempi_FOUND ) IF( TAGLIB_FOUND ) TARGET_LINK_LIBRARIES(translators ${TAGLIB_LIBRARIES}) ENDIF( TAGLIB_FOUND ) IF( CDIO_FOUND ) TARGET_LINK_LIBRARIES(translators ${CDIO_LIBRARIES}) ENDIF( CDIO_FOUND ) ADD_DEPENDENCIES(translators tellico_config) ########### install files ############### INSTALL(FILES bibtex-translation.xml DESTINATION ${TELLICO_DATA_INSTALL_DIR}) diff --git a/src/translators/librarythingimporter.cpp b/src/translators/librarythingimporter.cpp new file mode 100644 index 00000000..9ca38674 --- /dev/null +++ b/src/translators/librarythingimporter.cpp @@ -0,0 +1,161 @@ +/*************************************************************************** + Copyright (C) 2019 Robby Stephenson + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License or (at your option) version 3 or any later version * + * accepted by the membership of KDE e.V. (or its successor approved * + * by the membership of KDE e.V.), which shall act as a proxy * + * defined in Section 14 of version 3 of the license. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ***************************************************************************/ + +#include "librarythingimporter.h" +#include "../collections/bookcollection.h" +#include "../core/filehandler.h" +#include "../utils/string_utils.h" +#include "../utils/isbnvalidator.h" +#include "../tellico_debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using Tellico::Import::LibraryThingImporter; + +LibraryThingImporter::LibraryThingImporter() : Import::Importer(), m_widget(nullptr), m_URLRequester(nullptr) { +} + +bool LibraryThingImporter::canImport(int type) const { + return type == Data::Collection::Book; +} + +Tellico::Data::CollPtr LibraryThingImporter::collection() { + if(m_coll) { + return m_coll; + } + + if(!m_widget) { + myWarning() << "no widget!"; + return Data::CollPtr(); + } + + QUrl jsonUrl = m_URLRequester->url(); + + if(jsonUrl.isEmpty() || !jsonUrl.isValid()) { + myDebug() << "Bad jsonUrl:" << jsonUrl; + return Data::CollPtr(); + } + + + QByteArray data = Tellico::FileHandler::readDataFile(jsonUrl, false /* quiet */); + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if(doc.isNull()) { + myDebug() << "Bad json data:" << parseError.errorString(); + return Data::CollPtr(); + } + + m_coll = new Data::BookCollection(true); + Data::EntryList entries; + QVariantMap map = doc.object().toVariantMap(); + QMapIterator i(map); + while (i.hasNext()) { + i.next(); + QVariantMap valueMap = i.value().toMap(); + Data::EntryPtr entry(new Data::Entry(m_coll)); + entry->setField(QStringLiteral("title"), mapValue(valueMap, "title")); + entry->setField(QStringLiteral("pub_year"), mapValue(valueMap, "date")); + entry->setField(QStringLiteral("keyword"), mapValue(valueMap, "tags")); + entry->setField(QStringLiteral("genre"), mapValue(valueMap, "genre")); + entry->setField(QStringLiteral("series"), mapValue(valueMap, "series")); + entry->setField(QStringLiteral("language"), mapValue(valueMap, "language")); + + QJsonArray authorArray = valueMap.value(QStringLiteral("authors")).toJsonArray(); + QStringList authors; + for(int i = 0; i < authorArray.size(); ++i) { + QVariantMap m = authorArray.at(i).toObject().toVariantMap(); + // TODO: read config option for author formatting? + // use first-lastname for now + authors += mapValue(m, "fl"); + } + entry->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString())); + + QJsonArray formatArray = valueMap.value(QStringLiteral("format")).toJsonArray(); + for(int i = 0; i < formatArray.size(); ++i) { + QVariantMap m = formatArray.at(i).toObject().toVariantMap(); + const QString format = mapValue(m, "text"); + if(format == QLatin1String("Paperback")) { + entry->setField(QStringLiteral("binding"), i18n("Paperback")); + } else if(format == QLatin1String("Hardcover")) { + entry->setField(QStringLiteral("binding"), i18n("Hardback")); + } else { + // just in case there's a value there + entry->setField(QStringLiteral("binding"), format); + } + break; + } + + QString isbn = mapValue(valueMap, "originalisbn"); + ISBNValidator::staticFixup(isbn); + entry->setField(QStringLiteral("isbn"), isbn); + + // grab first set of digits + QRegularExpression digits(QStringLiteral("\\d+")); + QRegularExpressionMatch match = digits.match(mapValue(valueMap, "pages")); + if(match.hasMatch()) { + entry->setField(QStringLiteral("pages"), match.captured(0)); + } + entries += entry; + } + m_coll->addEntries(entries); + return m_coll; +} + +QWidget* LibraryThingImporter::widget(QWidget* parent_) { + if(m_widget) { + return m_widget; + } + m_widget = new QWidget(parent_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* gbox = new QGroupBox(i18n("LibraryThing Options"), m_widget); + QFormLayout* lay = new QFormLayout(gbox); + + lay->addRow(new QLabel(i18n("Export your LibraryThing collection in " + "JSON format."), gbox)); + + m_URLRequester = new KUrlRequester(gbox); + // these are in the old KDE4 filter format, not the Qt5 format + QString filter = QLatin1String("*.json|") + i18n("JSON Files") + + QLatin1Char('\n') + + QLatin1String("*|") + i18n("All Files"); + m_URLRequester->setFilter(filter); + + lay->addRow(i18n("LibraryThing file:"), m_URLRequester); + + l->addWidget(gbox); + l->addStretch(1); + + return m_widget; +} diff --git a/src/translators/translators.h b/src/translators/librarythingimporter.h similarity index 65% copy from src/translators/translators.h copy to src/translators/librarythingimporter.h index 57353472..ea2f33d9 100644 --- a/src/translators/translators.h +++ b/src/translators/librarythingimporter.h @@ -1,92 +1,62 @@ /*************************************************************************** - Copyright (C) 2003-2009 Robby Stephenson + Copyright (C) 2019 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ -#ifndef TRANSLATORS_H -#define TRANSLATORS_H +#ifndef TELLICO_IMPORT_LIBRARYTHINGIMPORTER_H +#define TELLICO_IMPORT_LIBRARYTHINGIMPORTER_H + +#include "importer.h" + +class KUrlRequester; namespace Tellico { namespace Import { - enum Format { - TellicoXML = 0, - Bibtex, - Bibtexml, - CSV, - XSLT, - AudioFile, - MODS, - Alexandria, - FreeDB, - RIS, - GCstar, - FileListing, - GRS1, - AMC, - Griffith, - PDF, - Referencer, - Delicious, - Goodreads, - CIW, - VinoXML, - BoardGameGeek - }; - enum Action { - Replace, - Append, - Merge - }; +/** + * @author Robby Stephenson +*/ +class LibraryThingImporter : public Importer { +Q_OBJECT + +public: + /** + */ + LibraryThingImporter(); + + virtual Data::CollPtr collection() Q_DECL_OVERRIDE; + virtual bool canImport(int type) const Q_DECL_OVERRIDE; - enum Target { - None, - File, - Dir - }; - } + virtual QWidget* widget(QWidget* parent) Q_DECL_OVERRIDE; - namespace Export { - enum Format { - TellicoXML = 0, - TellicoZip, - Bibtex, - Bibtexml, - HTML, - CSV, - XSLT, - Text, - PilotDB, // Deprecated - Alexandria, - ONIX, - GCstar - }; +public Q_SLOTS: + void slotCancel() Q_DECL_OVERRIDE {} - enum Target { - None, - File, - Dir - }; - } -} +private: + Data::CollPtr m_coll; + QWidget* m_widget; + KUrlRequester* m_URLRequester; +}; + } // end namespace +} // end namespace #endif diff --git a/src/translators/translators.h b/src/translators/translators.h index 57353472..76210c81 100644 --- a/src/translators/translators.h +++ b/src/translators/translators.h @@ -1,92 +1,93 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #ifndef TRANSLATORS_H #define TRANSLATORS_H namespace Tellico { namespace Import { enum Format { TellicoXML = 0, Bibtex, Bibtexml, CSV, XSLT, AudioFile, MODS, Alexandria, FreeDB, RIS, GCstar, FileListing, GRS1, AMC, Griffith, PDF, Referencer, Delicious, Goodreads, CIW, VinoXML, - BoardGameGeek + BoardGameGeek, + LibraryThing }; enum Action { Replace, Append, Merge }; enum Target { None, File, Dir }; } namespace Export { enum Format { TellicoXML = 0, TellicoZip, Bibtex, Bibtexml, HTML, CSV, XSLT, Text, PilotDB, // Deprecated Alexandria, ONIX, GCstar }; enum Target { None, File, Dir }; } } #endif