diff --git a/ChangeLog b/ChangeLog index dcf6a06b..92bbefd7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3711 +1,3723 @@ +2019-07-09 Robby Stephenson + + * Released Tellico 3.2.1. + +2019-07-06 Robby Stephenson + + * Fixed selection bug when modifying an entry after selection changed (Bug 391614). + +2019-07-01 Robby Stephenson + + * Fixed bug with not updating status bar when cancelling an entry update (Bug 325591). + 2019-06-16 Robby Stephenson * Fixed file preview generation. * Added data source for Kino-Teatr.ua. 2019-05-25 Robby Stephenson * Released Tellico 3.2. 2019-05-24 Robby Stephenson * Updated data source for TheGamesDB.net (Bug 407811). 2019-04-18 Robby Stephenson * Added data source for ComicVine.com. 2019-03-10 Robby Stephenson * Updated VNDB.org data source. 2019-02-24 Robby Stephenson * Corrected server for National Library of Lithuania (Bug 404743). 2019-02-20 Robby Stephenson * Added data source for MobyGames.com. 2019-02-14 Robby Stephenson * Updated IGDB data source to API v3. 2019-02-10 Robby Stephenson * Improved collection merging efficiency (Bug 349410). 2019-01-19 Robby Stephenson * Fixed bug reading title from KinoPoisk (Bug 403184). 2019-01-04 Robby Stephenson * Fixed bug with setting IMDB rating in various locales (Bug 401894). 2019-01-02 Robby Stephenson * Added Amazon sites for BR, AU, IN, MX, and TR. 2018-12-18 Robby Stephenson * Released Tellico 3.1.4. 2018-12-17 Robby Stephenson * Updated Kino.de data source. 2018-12-09 Robby Stephenson * Updated KinoPoisk.ru data source. 2018-10-30 Robby Stephenson * Updated bedetheque.com data source to use https. 2018-10-24 Robby Stephenson * Fixed bug to escape the group name when filtering in the Group View (Bug 399928). 2018-10-02 Robby Stephenson * Fixed bug when using a regular expression for filtering in a number field (Bug 399323). 2018-09-02 Robby Stephenson * Released Tellico 3.1.3. * Updated TheGamesDB data source. 2018-08-24 Robby Stephenson * Updated Amazon source to include multiple content listings in movie plots (Bug 396211). 2018-08-22 Robby Stephenson * Fixed LTR direction for year within Video template (Debian Bug #904259) 2018-07-08 Robby Stephenson * Fixed bug that cleared selection on modification when a filter was applied (Bug 394343). 2018-07-02 Robby Stephenson * Updated SRU fetcher to allow multiple marc records within a SRW result (DNB). 2018-04-18 Robby Stephenson * Updated IMDb data source (Bug 393239). 2018-04-02 Robby Stephenson * Updated BibTeXML importer. 2018-03-28 Robby Stephenson * Fixed compilation for Qt 5.6 (Bug 392457). 2018-03-28 Robby Stephenson * Released Tellico 3.1.2. * Updated Kino.de data source. 2018-03-18 Robby Stephenson * Fixed bug with inconsistent selection (Bug 391614). * Added a workaround for a crash when linking to both Exempi and KFileMetadata. 2018-02-24 Robby Stephenson * Updated IMDb data source. 2018-02-07 Robby Stephenson * Updated ISBNdb.com data source to v3. 2018-02-05 Robby Stephenson * Fixed "Filter by Group" (Bug 389931). 2018-01-30 Robby Stephenson * Updated MusicBrainz data source to Web Service v2. 2018-01-15 Robby Stephenson * Released Tellico 3.1.1. * Removed deprecated Wine.com data source. 2018-01-14 Robby Stephenson * Updated Kino.de data source. * Updated IGDB data source. 2018-01-09 Robby Stephenson * Fixed bug with OMDB settings not being saved (Bug 388703). * Updated IMDb data source. 2017-12-15 Robby Stephenson * Revert change to clear search dialog on new search (Bug 357799). 2017-11-19 Robby Stephenson * Fixed bug with filter selection (Bug 387130). 2017-11-17 Robby Stephenson * Fixed bug with multi-selection and entry editing (Bug 387053). 2017-11-12 Robby Stephenson * Fixed bug with incorrect entry titles in icon view and multiple entry icons (Bug 386548). * Fixed a few ISO-6937 character encodings. 2017-11-06 Robby Stephenson * Fixed bug for "losing" icons after modifying a collection (Bug 386549). 2017-11-02 Robby Stephenson * Fixed bug with duplicated colons in CSV importer (Bug 386483). 2017-11-01 Robby Stephenson * Improved "Filter by Group" to use a regular expression (Bug 386011). 2017-10-14 Robby Stephenson * Updated Goodreads importer to use https endpoint. 2017-10-13 Robby Stephenson * Released Tellico 3.1. 2017-09-20 Robby Stephenson * Fixed crashing bugs with remote image loading (Bug 382572, Bug 379607, Bug 384104). 2017-09-10 Robby Stephenson * Fixed bug with showing stars in column view for float values (Bug 384547). * Fixed bug with comparing float values (Bug 384547). 2017-08-26 Robby Stephenson * Fixed building with CMake 3.9+ (Bug 382680). 2017-07-16 Robby Stephenson * Added data source for kino.de. 2017-07-07 Robby Stephenson * Added message dialog for Amazon Associate warning (Bug 364784). 2017-07-06 Robby Stephenson * Added DBUS option for filtering exported entries (Bug 382035). 2017-07-02 Robby Stephenson * Fixed bug of running out of memory when writing very large XML files (Bug 380832). * Improved performance for filtering large collections by avoiding needless field value formatting. 2017-06-22 Robby Stephenson * Fixed bug with some icons not being shown (Bug 378477). 2017-05-29 Robby Stephenson * Switched from using libdiscid to libcdio, which includes cdtext. 2017-05-06 Robby Stephenson * Removed Freebase data source. 2017-05-01 Robby Stephenson * Fixed track length for CD audio (Bug 379426). 2017-04-30 Robby Stephenson * Added data source for IGDB.com. 2017-04-16 Robby Stephenson * Fixed bug showing icons for custom collection (Bug 378852). 2017-04-09 Robby Stephenson * Added data source for https://opensource.dbc.dk. * Added PEGI rating to GiantBomb results (Bug 375996). 2017-03-27 Robby Stephenson * Released Tellico 3.0.2. 2017-03-26 Robby Stephenson * Added data source for VideoGameGeek.com 2017-03-25 Robby Stephenson * Updated Douban data source to API v2 (from XML to JSON). 2017-03-24 Robby Stephenson * Fixed bug with image location for reports, introduced in v3.0.1 (Bug 377790). 2017-03-16 Robby Stephenson * Updated the IBS.it data source (Bug 373774). 2017-03-11 Robby Stephenson * Added data source for KinoPoisk.ru. 2017-03-04 Robby Stephenson * Added data source for OMDBAPI.com. 2017-02-20 Robby Stephenson * Released Tellico 3.0.1. 2017-02-12 Robby Stephenson * Removed Microsoft Academic Search data source (defunct). * Updated DBLP data source. 2017-02-08 Robby Stephenson * Fixed relative file locations in HTML export (Bug 376134). 2017-02-05 Robby Stephenson * Fixed image open dialog to remember last location (Bug 376002). * Added ESRB rating for "Everyone 10+" (Bug 375995). 2017-01-30 Robby Stephenson * Fixed Bibsys z39.50 settings (Bug 375758). * Fixed "Report Bug" menu item to link to bugs.kde.org (Bug 375760). * Update MARCXML2MODS stylesheet to version 3.6. 2017-01-29 Robby Stephenson * Fixed Google Scholar data source. 2017-01-15 Robby Stephenson * Fixed bug with Rating drawing size in list view (Bug 372560). 2016-12-19 Robby Stephenson * Fixed bug with truncated first two characters of root folder when importing a file listing (Bug 373918). 2016-11-08 Robby Stephenson * Released Tellico 3.0. 2016-06-15 Robby Stephenson * Added native data source for Bedetheque.com 2016-05-25 Robby Stephenson * Improved performance when deleting many entries. 2016-05-03 Robby Stephenson * Use elided text in Choice combobox to avoid wide windows (Bug 362028). 2016-04-26 Robby Stephenson * Added menu items for each Url field in icon view (Bug 250913). 2016-04-25 Robby Stephenson * Fixed crashing bug with empty table rows (Bug 361622). 2016-04-16 Robby Stephenson * Fixed filter comparison with custom dates (Bug 361625). 2016-01-15 Sven Werlen * Fixed "Defaults" button in Collection Fields dialog (Bug 357637). 2015-12-06 Robby Stephenson * Released Tellico 2.3.11. 2015-10-25 Robby Stephenson * Removed the Yahoo fetcher. 2015-10-22 Robby Stephenson * Added user-defined query pairs for SRUFetcher url. 2015-08-22 Robby Stephenson * Improved PDF importer for Science Direct files. 2015-08-16 Robby Stephenson * Fixed key accelerator conflict for 'D' in Collection Fields dialog (Bug 351226). 2015-08-05 Robby Stephenson * Fixed bug with using external fetcher source with MODS output. 2015-07-19 Robby Stephenson * Fixed a crashing bug with Nepomuk and certain file types (Bug 345458). 2015-06-02 Robby Stephenson * Fixed a bug with HTML exporting titles with single-quotes (Bug 348381). 2015-05-27 Robby Stephenson * Removed the Citebase fetcher. 2015-05-24 Robby Stephenson * Fixed a bug showing horizontal scroll bar with many table columns (Bug 348189). 2015-03-15 Robby Stephenson * Removed the PilotDB exporter. 2015-02-15 Robby Stephenson * Released Tellico 2.3.10. 2015-02-14 Robby Stephenson * Updated the Moviemeter data source to use the new JSON API. * Removed the local KXmlRpc copy since it is no longer needed. * Updated cover path in DarkHorse fetcher. 2015-01-23 Robby Stephenson * Updated the Discogs data source to use the new JSON API. 2014-12-22 Robby Stephenson * Added filter rules for numbers less than and greater. 2014-12-12 Robby Stephenson * Converted the BoardGameGeek fetcher from a script to the XML API. 2014-12-06 Robby Stephenson * Added importer for BoardGameGeek collections. 2014-12-05 Robby Stephenson * Fixed bug with importing Goodreads collection. 2014-11-09 Robby Stephenson * Added workaround for incorrect cover thumbnail in BoardGameGeek fetcher. 2014-10-20 Robby Stephenson * Updated CSV importer to recognize LibraryThing files. 2014-10-19 Robby Stephenson * Fixed bug with Allocine API search using punctuation (Bug 337432). 2014-10-11 Robby Stephenson * Added data source for Mathematical Review. * Fixed crashing bug with some ISBNdb results (Bug 339063). * Updated Producer results for IMDb and TheMovieDB fetchers (Bug 336765). 2014-10-11 Pino Toscano * Update internal libcsv copy to 3.0.3. 2014-10-09 Pino Toscano * Switch to using libdiscid for reading audio CD info. 2014-06-22 Robby Stephenson * Released Tellico 2.3.9. 2014-05-26 Robby Stephenson * Fixed bug with writing out link-only images in HTML exporter (Bug 330649). 2014-05-25 Robby Stephenson * Fixed character encoding in Allocine fetcher (Bug 334527). * Removed IMDb country choice since it's now unavailable (Bug 330641). 2014-04-27 Robby Stephenson * Disabled Discogs image fetching since OAuth is now required. 2014-04-17 Robby Stephenson * Fixed CSV importer bug, causing a hang (Bug 329677, Debian Bug 729503). 2014-01-31 Robby Stephenson * Updated TheMovieDB fetcher to API version 3. 2014-01-30 Robby Stephenson * Fixed crashing bug in IMDB fetcher (Bug 330591). 2013-11-14 Robby Stephenson * Fixed crashing bug in command-line importing (Debian bug 729499). 2013-11-08 Robby Stephenson * Fixed type error in Microsoft Academic Search fetcher. 2013-11-07 Robby Stephenson * Fixed error in setting modified date for entry in certain cases (Bug 326911). 2013-11-06 Robby Stephenson * Improved GCstar import and export to handle custom GCstar fields. 2013-10-13 Robby Stephenson * Updated IMDb fetcher (Bug 325673). 2013-07-07 Robby Stephenson * Released Tellico 2.3.8. 2013-07-04 Robby Stephenson * Disabled fetchers for Spanish Ministry of Culture, Beyazperde, Filmstarts, ScreenRush, and Sensacine. 2013-07-01 Robby Stephenson * Updated ISBNdb.com fetcher to use v2 of API. 2013-06-09 Robby Stephenson * Added Bibtex importing for drag/drop text (Bug 319182). 2013-06-08 Robby Stephenson * Fixed Entrex/Pubmed fetcher for summary requests (Bug 319501). 2013-04-05 Robby Stephenson * Fixed bug with retaining allowed values when adding entries from data sources (Bug 317905). 2013-03-27 Robby Stephenson * Fixed bug with not properly escaping text in CSV exporter (Bug 317473). 2013-03-24 Robby Stephenson * Updated GiantBombFetcher for XML changes in responses. 2013-03-11 Robby Stephenson * Fixed regexp in Google Scholar fetcher to set cookie (Bug 316550). 2013-03-10 Robby Stephenson * Update Allocine API to use JSON instead of XML. 2013-03-03 Robby Stephenson * Add coverartarchive.org to MusicBrainz data source. 2013-02-09 Robby Stephenson * Fixed character encoding in IMDB results list (Bug 314113). 2013-02-02 Robby Stephenson * Added fetcher for VNDB.org. 2013-01-27 Robby Stephenson * Added Dewey Decimal and LCC to ISBNdb.org results. 2013-01-26 Robby Stephenson * Fixed crashing bug with OpenLibrary fetcher. 2013-01-23 Robby Stephenson * Released Tellico 2.3.7. 2013-01-18 Robby Stephenson * Fixed a bug with the number field not catching a modified value (Bug 313304). 2013-01-08 Robby Stephenson * Fixed a bug with editing the toolbar config in KDE 4.9.2+. 2012-12-07 Robby Stephenson * Added a config setting for using a Google API key for book searches. * Updated fetcher for IMDb. 2012-11-21 Robby Stephenson * Added VinoXML importer. 2012-11-03 Robby Stephenson * Added fetcher for DBLP.org. 2012-10-14 Robby Stephenson * Fixed a bug that caused loans to fail to get checked-in (Bug 307958). 2012-09-18 Robby Stephenson * Added fetcher for thegamesdb.net. 2012-09-09 Robby Stephenson * Changed Griffith importer to use XML files instead of python script and assuming a sqlite3 database. 2012-09-03 Robby Stephenson * Updated Delicious Library importer. 2012-08-26 Robby Stephenson * Removed API key configuration for Discogs since it's no longer needed. 2012-08-22 Robby Stephenson * Corrected character encoding for DVDfr search. * Corrected title vs. original title in DVDfr results. * Corrected actor and role order in Allocine fetcher. 2012-08-07 Robby Stephenson * Fixed a bug with adding new fields when importing bibtex (Bug 304767). 2012-07-13 Robby Stephenson * Released Tellico 2.3.6. 2012-06-10 Robby Stephenson * Added simple importer for CIW files from ISI. 2012-05-30 Robby Stephenson * Added data sources for IMDB in French, Spanish, German, Italian, and Portuguese. 2012-05-12 Robby Stephenson * Fixed a bug with loading images from Google Book search (Bug 299789). 2012-05-06 Robby Stephenson * Added API data source for HathiTrust. 2012-05-04 Robby Stephenson * Fixed a bug when using a local image directory for a file that has not yet been saved (Bug 299130). 2012-04-20 Robby Stephenson * Added API data sources for Allocine, ScreenRush, FilmStarts, SensaCine, and Beyazperde. 2012-03-23 Robby Stephenson * Fixed bugs in IBS.it data source. 2012-03-14 Robby Stephenson * Added data source for Springer Link. 2012-03-09 Robby Stephenson * Added data source for the Microsoft Academic Search. 2012-02-26 Robby Stephenson * Fixed a crashing bug when entering multiple ISBN values in search. * Improved Amazon search for US UPC values from non-UPC sites. 2012-01-15 Robby Stephenson * Fixed a crashing bug when using the scanner dialog. * Released Tellico 2.3.5. 2011-12-26 Robby Stephenson * Fixed bug with merging entries. Entry IDs will never be different so don't warn the user (Bug 289346). * Fixed bug with Cancel not working in the Entry Merge dialog. 2011-12-17 Robby Stephenson * Refactored fetcher tests to check for network access before running * Updated Discogs fetcher to use API v2 2011-12-14 Robby Stephenson * Fixed importing of multi-line notes from Alexandria (Bug 289022). 2011-11-30 Robby Stephenson * Updated fetcher for Allocine.fr. 2011-10-28 Robby Stephenson * Added filter rules for dates before and after. 2011-10-13 Robby Stephenson * Fixed broken script for searching Dark Horse Comics. 2011-10-05 Robby Stephenson * Fixed bug that always showed checkmark for boolean field values (Bug 283444). 2011-10-04 Robby Stephenson * Updated GCstar exporter to include images. 2011-09-27 Robby Stephenson * Added Amazon data sources for China, Spain, and Italy. 2011-09-24 Robby Stephenson * Released Tellico 2.3.4. 2011-09-18 Robby Stephenson * Fixed bug when editing month in the date widget in KDE 4.7 (Bug 281365). 2011-08-30 Robby Stephenson * Fixed showing a doubled filter count (Bug 281082). 2011-08-21 Robby Stephenson * Added data source for Google Book search. 2011-07-23 Robby Stephenson * Updated filter rules to match against values without diacritics for "Contains" criters (Bug 222400). 2011-07-20 Robby Stephenson * Fixed bug in year from Douban.com data source. * Fixed TMDB search for multiple person results. 2011-07-01 Robby Stephenson * Added importer for MovieMeter.nl. 2011-06-24 Robby Stephenson * Fixed crashing bug with CSV importer and changing delimiters. 2011-05-29 Robby Stephenson * Made Entry View always visible. * Moved icon view to share space with the list view (Bug 250912). 2011-05-20 Robby Stephenson * Added importer for Goodreads collections. 2011-05-13 Pedro Miguel Carvalho * Added shortcut key to switch main window to/from full screen state. * Added shortcut key to show/hide menubar (Bug 251157). 2011-05-07 Pedro Miguel Carvalho * Added UI controls for changing icon size (Bug 250907). 2011-05-07 Robby Stephenson * Fixed bug with image link in Tri-Column report template (Bug 272744). 2011-05-06 Robby Stephenson * Added French, Spanish, and German codes for TheMovieDB. * Added icon cache to improve performance with large images (Patch from Pedro Miguel Carvalho, Bug 272583). 2011-05-01 Robby Stephenson * Changed image loading to use known id instead of recalculating, improves performance. 2011-04-10 Robby Stephenson * Improved performance for loading and sorting large collections. * Released Tellico 2.3.3. 2011-04-04 Robby Stephenson * Fixed bug with loans not being updated for removed entries (Bug 270129). 2011-03-21 Robby Stephenson * Fixed crashing bug for editing some values (Bug 269044). 2011-03-19 Robby Stephenson * Fixed editing existing filters (Bug 268829). 2011-03-07 Robby Stephenson * Added Manga search for AnimeNfo. 2011-02-27 Robby Stephenson * Fixed parsing bug with AnimeNfo results. 2011-02-23 Regis Boudin * Updated videodev include for compatibility with Linux kernel 2.6.38+. 2011-02-21 Robby Stephenson * Added data source for douban.com. 2011-02-19 Robby Stephenson * Added data source fetcher for filmaster.com. 2011-02-18 Regis Boudin * Fixed build with GCC 4.6. 2011-02-13 Robby Stephenson * Added check for duplicate bibtex keys (Bug 245225). 2011-02-02 Robby Stephenson * Added capability to import ADS format for z39.50 sources. 2011-01-11 Robby Stephenson * Fixed bug with parsing empty table values (Bug 261108). 2011-01-03 Robby Stephenson * Fixed parsing bug with IMDb results (Bug 262036). 2010-12-12 Robby Stephenson * Released Tellico 2.3.2. 2010-12-11 Robby Stephenson * Updated Bibtex handler to translate ~ to non-breaking space. 2010-12-08 Robby Stephenson * Updated Allocine script to version 0.7.3 (Bug 258281). 2010-12-03 Robby Stephenson * Fixed sorting multiple numeric values in column view. * Fixed sorting for numeric values in group view. 2010-12-01 Robby Stephenson * Fixed bug with FreeDB results not using track artists (Bug 258541). * Fixed bug with importing Bibtex file with keyword and keywords fields (Bug 258269); 2010-11-23 Robby Stephenson * Fixed Google Scholar fetcher to properly set bibtex cookie. 2010-11-21 Robby Stephenson * Added cover art to MusicBrainz fetcher. 2010-11-20 Robby Stephenson * Switched Nepomuk requirement to optional for file metadata. 2010-11-14 Robby Stephenson * Fixed bug with updating groups for derived values (Bug 256374). 2010-11-10 Robby Stephenson * Fixed bug with list view settings not being saved between sessions (Bug 256373). 2010-11-06 Robby Stephenson * Released Tellico 2.3.1. 2010-11-02 Robby Stephenson * Fixed edit dialog behavior when closing to be consistent with discarding data (Bug: 255938). 2010-10-21 Robby Stephenson * Fixed error in Italian translation that caused HTML error and add workaround for future problems (Bug: 254863). 2010-10-17 Robby Stephenson * Updated Freebase source for adapted /music/release schema. 2010-10-14 Robby Stephenson * Updated IMDb data source for new layout (Bug: 253549). 2010-10-13 Robby Stephenson * Added options in CSV importer and exporter for table delimiters. 2010-09-20 Robby Stephenson * Changed URL field output to truncate link text to 30 letters (Bug: 250880). 2010-09-12 Robby Stephenson * Increased max icon size to 256. 2010-09-11 Robby Stephenson * Changed updating match algorithm to try to improve results when having multiple good matches (Bug: 250886). 2010-09-10 Robby Stephenson * Updated GCstar plugin fetcher to use a separate thread. * Fixed crash with z39.50 fetcher (Bug: 250795). 2010-09-07 Robby Stephenson * Updated GCstar plugin fetcher to work with comic books. 2010-09-03 Robby Stephenson * Fixed parsing for director and writer in IMDB fetcher (Bug: 249096). 2010-09-02 Robby Stephenson * Updated DTD and added unit test for validation. 2010-08-29 Robby Stephenson * Added GCstar import/export for comic book collections. 2010-08-28 Robby Stephenson * Fixed bug with adding new fields during CSV import. 2010-08-25 Robby Stephenson * Added GCstar export for wine collections. * Improved GCstar import for wine collections. 2010-08-21 Robby Stephenson * Fixed filter view to apply filter when item is selected (Bug: 248657). 2010-08-16 Robby Stephenson * Improved exporter to add option for limiting exported fields (Bug 246390). 2010-08-13 Robby Stephenson * Added data source fetcher for dvdfr.com. 2010-08-07 Robby Stephenson * Released Tellico 2.3. 2010-07-29 Robby Stephenson * Fixed sorting by rating when rating has double digits (Bug: 246202). 2010-07-25 Robby Stephenson * Fixed IMDb matching on partial titles (Bug: 245665). * Fixed IMDb fetching of alternative titles. 2010-07-11 Robby Stephenson * Fixed IMDb fetching of audio track and color (Bug: 244159). 2010-07-10 Robby Stephenson * Fixed metadata import for file listing. 2010-07-09 Robby Stephenson * Added Dewey Decimal and LoC Classification to z3950 MODS import. * Fix subtitle value in MODS import. 2010-06-19 Robby Stephenson * Added filter for data source list. 2010-05-04 Robby Stephenson * Updated BoardGameGeek fetcher for new API. 2010-05-02 Robby Stephenson * Added data source for Freebase. 2010-04-29 Robby Stephenson * Fixed crashing bug for loading images whose size exceeded the cache size. 2010-04-28 Robby Stephenson * Changed Amazon Japan video search to include DVDs. * Added new fetcher for combining results from multiple sources. 2010-04-27 Robby Stephenson * Updated allocine.fr script to version 0.7.1, patch from Romain Henriet. 2010-03-29 Robby Stephenson * Fixed bug with cursor position in date field entry. 2010-03-27 Robby Stephenson * Added fetcher for openlibrary.org (requires QJSON lib). 2010-03-24 Robby Stephenson * Added clear button for rating widget (BUG: 227982). * Control characters are now stripped from all text files on import (BUG: 231302). * Removed local FindKcddb cmake module in favor of system FindKdeMultimedia. 2010-03-23 Robby Stephenson * Fixed selection bug when duplicating an entry (BUG: 231125). 2010-03-22 Robby Stephenson * Fixed crash in exporting HTML for collections with no grouping field (BUG: 231302). * Fixed bug that showed info dialog multiple times when changing image location. 2010-02-24 Robby Stephenson * Fixed crash in CSV importer. 2010-02-21 Robby Stephenson * Tweaked entry ID values to start at 1 instead of 0. 2010-02-13 Robby Stephenson * Released Tellico 2.2. 2010-02-06 Robby Stephenson * Enabled Korganizer integration for tracking loans. 2010-01-27 Robby Stephenson * Fixed some hyphenation problems when typing 978 ISBN values. * Enabled KAddressBook integration for borrowers. 2010-01-15 Robby Stephenson * Fixed bug in en_GB translation that affected file selection. 2010-01-13 Robby Stephenson * Enabled filter dialog to apply empty filter. 2010-01-03 Robby Stephenson * Improved performance for modifying many entries at once. 2010-01-01 Robby Stephenson * Added Giant Bomb data source. 2009-12-31 Robby Stephenson * Fixed adding "link-only" files (BUG #220645). * Fixed Discogs track download and title search. 2009-12-22 Robby Stephenson * Updated CrossRef data source to allow authentication via email address. * Updated Crossref data source for new schema. 2009-12-19 Robby Stephenson * Added MusicBrainz data source. 2009-12-18 Robby Stephenson * Fixed setting correct permissions of backup file (BUG #219259). * Fixed formatting of multiple people with auto-formatting (BUG #219268). 2009-12-16 Robby Stephenson * Added themoviedb.org data source. 2009-12-12 Robby Stephenson * Improve updating from Amazon to include book title or album title in search. 2009-12-04 Robby Stephenson * Added option for turning webcam off. 2009-11-30 Robby Stephenson * Enabled JavaScript for report dialog, incase a template wants to use it. * Fixed crashing bug for sorting during HTML export. * Fixed multiple selection in entry view (KDE #216122). 2009-11-20 Robby Stephenson * Fixed IMDB search to find director and writers. * Fixed Polish ISBN formatting. * Released Tellico 2.1.1. 2009-11-17 Robby Stephenson * Fixed sorting in icon view. 2009-11-16 Robby Stephenson * Fixed bug that kept filters from being saved. BUG#214672. 2009-11-12 Robby Stephenson * Updated script for searching allocine.fr. 2009-11-11 Robby Stephenson * Changed webcam image preview to only appear when searching by ISBN or UPC/EAN. 2009-11-10 Robby Stephenson * Fixed sorting when auto-formatting is on. 2009-11-06 Robby Stephenson * Fixed crashing bug related to some table values. * FIxed crashing bug related to column sorting. BUG#214661. 2009-11-01 Robby Stephenson * Released Tellico 2.1. 2009-10-31 Robby Stephenson * Fixed crashing bug for loading ISBN list from file. 2009-10-22 Robby Stephenson * Fixed bug with the sort order not being saved. 2009-10-21 Robby Stephenson * Fixed bug that auto-formatting was not always applied. 2009-10-11 Robby Stephenson * Fixed grouping to always show group for empty value. * Fixed crashing bug when filtering entry with empty title. * Fixed compilation for KDE < 4.2. * Fixed filtering bug when modifying entry value. 2009-10-10 Robby Stephenson * Added PAM/PRISM translator to SRU fetcher. 2009-10-07 Robby Stephenson * Updated Amazon and Crossref search to no longer store key in the KWallet. 2009-10-03 Robby Stephenson * Fixed bug to allow multiple values in table columns. 2009-09-30 Robby Stephenson * Added Wine.com data source. 2009-09-29 Robby Stephenson * Updated GCstar import support for video games and board games. 2009-09-24 Robby Stephenson * Updated ISBNdb.com fetcher and Discogs fetcher to allow user access keys. 2009-09-23 Robby Stephenson * Updated the SRU fetcher to leave the response format empty. 2009-09-22 Robby Stephenson * Fixed crash when exporting to HTML under certain conditions. * Removed the GCcfilms exporter and added the GCstar exporter. 2009-09-21 Robby Stephenson * Fixed typo in DTD. 2009-09-20 Robby Stephenson * Released Tellico 2.0. 2009-09-07 Robby Stephenson * Enabled multiple ISBN and LCCN search for SRU fetcher. 2009-09-06 Robby Stephenson * Updated the "Multiple ISBN" dialog box to validate ISBN and CueCat input. 2009-09-04 Robby Stephenson * Updated webcam support to use libv4l library. 2009-09-01 Robby Stephenson * Released Tellico 2.0pre2. 2009-08-23 Robby Stephenson * Added default fields for entry creation date and last-modified date. * Removed dependency on QCA2 library. 2009-08-09 Robby Stephenson * Updated Tellico user mailing list address. * Released Tellico 2.0pre1. 2009-08-01 Robby Stephenson * Updated HTML export to use country and language code both for sorting. * Updated Tellico home page URL. * Moved to document syntax 11 for removing Dependent and ReadOnly fields. * Added ID field to each new collection by default. 2009-03-13 Petri Damstén * Added scanning support to image widget. * Added 'Open With...' -button to image widget 2009-03-10 Robby Stephenson * Added reading album artist from TPE2 tags in mp3 files. 2009-02-14 Robby Stephenson * Released Tellico 1.3.5. 2009-02-04 Robby Stephenson * Reverted change from 2007 that merged entries by combining all values in multiple-value fields. 2009-02-02 Robby Stephenson * Fixed the CueCat decoder to work for ISBN searches, as well as UPC. 2009-01-30 Robby Stephenson * Updated Deliciour Library 1 importer to handle movies and games. * Fixed Ubuntu bug#317822, don't mark collection modified when image is found in local data directory. * Fixed query bug with z39.50 ISBN search. 2008-12-07 Robby Stephenson * Updated it.po, thanks to Valerio Ricci. 2008-10-23 Robby Stephenson * Added date, time, and username as available params for the XSLT export. 2008-09-13 Robby Stephenson * Released Tellico 1.3.4. 2008-09-04 Robby Stephenson * Fixed error with consecutive tabs in CSV import. 2008-08-26 Robby Stephenson * Fixed bug with image links in reports not being linked correctly. 2008-08-20 Robby Stephenson * Added minimal searching for board games from Amazon. 2008-08-07 Robby Stephenson * Changed Choice fields to sort by position in list. 2008-07-25 Robby Stephenson * Added ESC key for clearing quick filter. * Improved drag/drop to match on file extension after checking mimetype. 2008-07-22 Robby Stephenson * Updated IMDb plot regexp. 2008-07-09 Robby Stephenson * Fixed EntryView to honor copy() command properly, for clipboard. * Released Tellico 1.3.3. 2008-06-24 Robby Stephenson * Updated Porbase URL in z39.50 server list. 2008-06-13 Robby Stephenson * Updated drag-and-drop to allow HTTP urls. 2008-06-12 Robby Stephenson * Changed Arxiv fetcher to remove ID version number for results. 2008-06-07 Robby Stephenson * Fixed bug with merging file catalogs, to properly match on URL. 2008-05-24 Robby Stephenson * Released Tellico 1.3.2.1. * Fixed en_GB translation to remove context strings. 2008-05-23 Robby Stephenson * Released Tellico 1.3.2. * Include Canadian LCC values, patch from Peter Stevenson. 2008-05-19 Robby Stephenson * Updated entry creation to always add default values. Fixes Debian bug 481639. 2008-04-29 Robby Stephenson * Improved IMDb search results. 2008-04-15 Robby Stephenson * When adding a book to a bibliography, set the type to 'book'. 2008-04-11 Robby Stephenson * Updated LCCN search to validate format. * Updated entry match to compare validated LCCN. * Updated z39.50 search to allow multiple LCCN values. 2008-04-09 Robby Stephenson * Changed loader to eliminate possible duplicates for fields allowing multiple values (like keywords). * Updated PubMed search to add all keywords. * Fixed PubMed search to use utf8 encoding. 2008-04-08 Robby Stephenson * Changed XML loader to strip whitespace from non-paragraph values. 2008-04-06 Robby Stephenson * Added languages and editors to Amazon book import. 2008-03-15 Robby Stephenson * Updated mods2tellico.xsl to try to capture more of mods info converted from refbase. 2008-03-13 Robby Stephenson * Updated mods2tellico.xsl to try to capture more of bibtex info converted from wikindx. 2008-03-10 Robby Stephenson * Released Tellico 1.3.1. * Added Discogs.com fetcher. 2008-03-06 Robby Stephenson * Added Google Scholar search. 2008-03-05 Robby Stephenson * Fixed bug that prevented using bibtex format for external scripts. 2008-03-03 Robby Stephenson * Changed "ISBN not found" dialog to only appear if multiple ISBN values are being searched. 2008-03-01 Robby Stephenson * Fixed bug with SRU format not getting remembered in config dialog. * Fixed bug with entries with multiple titles not getting linked in the HTML export. 2008-02-29 Robby Stephenson * Added LCCN lookup to z39.50 source. * Added LCCN lookup to SRU source. * Updated CrossRef fetcher to use new unixref format with more data. 2008-02-26 Robby Stephenson * Added DOI search to Entrez/Pubmed interface. * Fixed bug with some free-form date fields not getting formatted correctly, resulting in a blank string in column view and reports. 2008-02-12 Robby Stephenson * Improved loader to delay loading linked images as long as possible. 2008-02-11 Robby Stephenson * Fixed Bibtex import for keywords field. 2008-02-07 Robby Stephenson * Updated Delicious Library importer to look for cover images. * Updated BoardGameGeek script to grab cover image, patch from Sven Werlen. 2008-01-29 Robby Stephenson * Released Tellico 1.3. * Fixed bug with stepping through selected entries after editing one. 2008-01-20 Robby Stephenson * Fixed bug with matching ISBN values for updating entries. 2008-01-11 Robby Stephenson * Released Tellico 1.3pre1. 2008-01-09 Robby Stephenson * Updated Spanish Ministry of Culture script. 2008-01-05 Robby Stephenson * Updated IMDB search to fix director and TV cast. 2007-12-11 Robby Stephenson * Added DCOP calls to set the view filter and to show entries. * Improved filter to allow specifying field name with '='. 2007-12-10 Robby Stephenson * Added importer for Delicious Library files. 2007-12-06 Robby Stephenson * Updated Dependant field parsing to allow %{id} to grab entry id. * Added context menu option in icon view to change sorting. * Extended the DCOP interface to allow adding and removing entries, reading field values, and setting field values. 2007-11-30 Robby Stephenson * Added capability to save a link to an image only, instead of saving it to the users image directory. Setting an image field property link=true will make that the default. 2007-11-26 Robby Stephenson * Updated to allow a "required" property to be included to indicate that a on-empty value should always be present when saving. * Updated to use libcsv from Robert Gamble, allowing newlines 2007-11-25 Robby Stephenson * Fixed bug with creating filter from group item. * Added Ukrainian translation from Serhij Dubyk Сергій Дубик. 2007-11-15 Robby Stephenson * Added a sharedmimeinfo file. 2007-11-13 Robby Stephenson * Added Bibsonomy fetcher. * Added citebase.org fetcher. 2007-11-06 Robby Stephenson * Added option for importing bibtex in Unicode (UTF-8). * Added importing from Referencer. 2007-11-05 Robby Stephenson * Added arxiv.org API fetcher. 2007-11-04 Robby Stephenson * Fixed bugs related to compilation with gcc 4.3. 2007-10-27 Robby Stephenson * Added option for saving images in local relative directory to data file. 2007-10-26 Robby Stephenson * Added menu command for merging entries, based on patch from Cyril Dangerville. 2007-10-24 Robby Stephenson * Added CrossRef search for DOI. 2007-10-21 Robby Stephenson * Fixed list view sorting to take into account automatic formatting. * Fixed date sorting to put empty dates first. 2007-10-19 Robby Stephenson * Fixed sorting bug for images. * Fixed sorting bug for empty dates in HTML export, patch from Jake Maciejewski. 2007-10-15 Robby Stephenson * Fixed crashing bug in bibtex importer. 2007-10-14 Robby Stephenson * Added PDF importer, using exempi XMP library. * Added drag & drop support for PDF importer. 2007-10-13 Robby Stephenson * Added DOI and URL to default bibtex collection fields. 2007-09-23 Robby Stephenson * Added data source for using GCstar plugins. * Fixed bug with escaping the image id for file names. 2007-09-22 Robby Stephenson * Released Tellico 1.2.14. 2007-09-20 Robby Stephenson * Added importer for Griffith database. 2007-09-10 Robby Stephenson * Updated Spanish Ministry of Culture script to search out of prints book and add notes field. * Updated tellico2html.js file for searching in HTML export. * Updated GCstar import to parse new XML format in GCstar version 1.2. 2007-08-27 Robby Stephenson * Updated allocine.fr source script to version 0.4, thanks to Mathias Monnerville. * Updated the Amazon.com search to allow searching for comic books. * Updated the isbndb.com search to allow searching for comic books. * Fixed ISBNdb search. 2007-08-25 Robby Stephenson * Changed default SRU port from 7090 to 80. 2007-08-23 Robby Stephenson * Update CSS in HTML export. 2007-08-10 Robby Stephenson * Fixed bug with yaz_iconv not flushing complete string for z39.50 search. 2007-08-09 Robby Stephenson * Added MARCXML to possible SRU format. * Fixed bug with MARC stylesheets to work with better with embedded records. 2007-08-03 Robby Stephenson * Updated entry updating to work with all collection types. 2007-07-28 Robby Stephenson * Released Tellico 1.2.13. 2007-07-24 Robby Stephenson * Added spell-check for text and paragraph fields. 2007-07-22 Robby Stephenson * Added compatibility with yaz3. 2007-07-19 Robby Stephenson * Added importer for GCstar files. * Fixed bug with namespace handling in Tellico XML loading. 2007-07-14 Robby Stephenson * Added Copac and National Library of Lithuania to z39.50 list. 2007-07-08 Robby Stephenson * Fixed crashing bug when adding items to a collection with loans. 2007-07-04 Robby Stephenson * Released Tellico 1.2.12. * Changed z39.60 search event loop in an attemp to fix some intermittent freezes. 2007-06-26 Robby Stephenson * Changed Quick Filter to split text on white space and do an AND search. * Bumped automake requirement to version 1.8 or later for macro to work. * Fixed HTML export to not rewrite file location for files referenced in the XSL file which don't exist. * Fixed Column View report to sort numerically when needed. * Fixed Date fields to suppress empty date values. * Fixed Date comparisons to work for single digits, patch from Jake Maciejewski. * Fixed Fields Dialog to show warnings when clicking Ok. 2007-06-22 Robby Stephenson * Added '\%' to bibtex translation table for comment escaping. 2007-05-30 Robby Stephenson * Fixed potential recursion bug for dependent fields. 2007-05-26 Robby Stephenson * Fixed bug that didn't write image size options when printing. 2007-05-09 Robby Stephenson * Added Turkish translation, thanks to Ali Isingor. 2007-05-08 Robby Stephenson * Released Tellico 1.2.11. 2007-05-06 Robby Stephenson * Improved file saving performance by caching image info on load. 2007-05-04 Robby Stephenson * Fixed bug that lost some images when loading directly from XML. 2007-04-13 Robby Stephenson * Fixed sorting for Dependent fields to match on subfields. 2007-04-12 Robby Stephenson * Fixed BibteXML export to include author. * Fixed book collection conversion to bibliography to set entry type. 2007-04-10 Robby Stephenson * Updated IMDb search for director and writer. * Fixed CDDB lookup on OpenBSD, patch from Marc Espie. 2007-04-07 Robby Stephenson * Released Tellico 1.2.10. 2007-04-03 Robby Stephenson * Improved accuracy and comprehensiveness of merging collections. * Changed collection merge to concatenate paragraph fields. * Fixed bug that hid some field groups after undoing append collection. 2007-04-01 Robby Stephenson * Added Blu-ray and HD DVD to video formats. 2007-03-31 Robby Stephenson * Updated ibs.it searching. * Fixed searching to replace all HTML entities. * Fixed character encoding for Alexandria import. 2007-03-29 Robby Stephenson * Updated Amazon API to 2007-02-22. * Fixed error handling for ISBN-13 searches. * Fixed bug that showed "ISBN Not Found" dialog in error. 2007-03-27 Robby Stephenson * Cleaned-up up ONIX export a bit. * Fixed Alexandria export. * Fixed importing covers from Alexandria. 2007-03-24 Robby Stephenson * Fixed bug that caused some amazon results to be hidden when repeating a search. 2007-03-20 Robby Stephenson * Fixed hanging bug when stopping a z39.50 search. 2007-03-14 Robby Stephenson * Fixed crashing bug when undoing a collection import or replacement. 2007-03-01 Robby Stephenson * Released Tellico 1.2.9. 2007-02-25 Robby Stephenson * Relaxed MODS processing to allow any result with typeOfResource="text" * Fixed bug with adding space after commas in edit widget. * Fixed bug with formatting of dependent fields always being capitalized. 2007-02-20 Robby Stephenson * Updated IMDb parsing for alternative titles and certifications. 2007-02-06 Robby Stephenson * Fixed sorting for rating groups with rating = 10. 2007-02-03 Robby Stephenson * Released Tellico 1.2.8. 2007-01-22 Robby Stephenson * Updated CDDB import to grab extd data as comments, and category as keyword. 2007-01-21 Robby Stephenson * Updated audio file importer to take disc number into account for mp3, ogg, and flac files. 2007-01-13 Robby Stephenson * Changed IMDB rating to allow float values. * Fixed compile error for z3950 connection, patch from Markus Brueffer. 2007-01-04 Robby Stephenson * Fixed cleanup for deleting scripts installed from newstuff. 2007-01-03 Robby Stephenson * Fixed busy cursor hanging in newstuff download dialog. 2006-12-21 Robby Stephenson * Fixed "ISBN Not Found" dialog to have selectable text. 2006-12-19 Robby Stephenson * Fixed bug with comparing relative URLs for merging file catalogs. 2006-12-17 Robby Stephenson * Fixed bug with some results in z39.50 search not showing up, due to search events getting processed out of order. 2006-12-07 Robby Stephenson * Fixed bug with secondary and tertiary sorting. 2006-12-06 Robby Stephenson * Bumped the Amazon ECS version to 2006-11-30. * Updated Amazon search to allow ISBN-13 values. * Fixed bug with saving URLs without the protocol. * Changed entry comparison for files to match only on URL value. * Released Tellico 1.2.7. 2006-12-04 Robby Stephenson * Improved performance for copying and deleting multiple entries. 2006-11-30 Robby Stephenson * Updated sorting to take title articles into account. 2006-11-29 Robby Stephenson * Added ISBN hyphenation rules for German, Czech, and Dutch. * Fixed bug that caused image loss when loading from data file. 2006-11-10 Robby Stephenson * Released Tellico 1.2.6. 2006-11-09 Robby Stephenson * Improved TV show matching for IMDB. 2006-11-05 Robby Stephenson * Fixed crashing bug in ProgressItem, reported by Izaak Branderhorst. 2006-11-03 Robby Stephenson * Released Tellico 1.2.5. 2006-11-01 Robby Stephenson * Fixed another bug with over-writing images on entry update. 2006-10-24 Robby Stephenson * Updated yahoo audio search webservice url. 2006-10-18 Robby Stephenson * Added warning when importing CSV without assigning any fields. * Updated allocine script (v 0.3), from Mathias Monnerville. 2006-10-16 Robby Stephenson * Added board game collection, patch from Steve Beattie. * Added script for searching boardgamegeek.com, patch from Steve Beattie. 2006-10-15 Robby Stephenson * Branched 1.2.x series. * Fixed encoding bug with allocine.fr script. 2006-10-14 Robby Stephenson * Released Tellico 1.2.4. 2006-10-12 Robby Stephenson * Fixed bug with overwriting images when updating entries. 2006-10-11 Robby Stephenson * Improved performance on loading and unloading. * Changed Nintendo 'Revolution' to 'Wii'. 2006-10-05 Robby Stephenson * Fixed off-by-one error in file listing volume name reader. * Update allocine fetcher. 2006-09-25 Robby Stephenson * Fixed bug with fetch dialog showing all sources, instead of just relevant ones. 2006-09-24 Robby Stephenson * Released Tellico 1.2.3. 2006-09-20 Robby Stephenson * Added button to search dialog to get additional results. 2006-09-14 Robby Stephenson * Updated to work with KDE 3.3.1 or later. 2006-09-09 Robby Stephenson * Fixed bug with HTML export not including tellico2html.js. 2006-09-07 Robby Stephenson * Fixed bug that could cause loss of images. * Fixed URL output in Image List report. * Released Tellico 1.2.2. 2006-09-06 Robby Stephenson * Released Tellico 1.2.1. 2006-09-05 Robby Stephenson * Fixed CDDB cache importer to always assume utf-8, as in libkcddb. * Changed bibtex exporter to add braces or quotes around url fields. 2006-09-04 Robby Stephenson * Fixed infinite loop in file listing importer. * Increased default image cache size to 10 meg. * Reworked image loading to avoid reloading large images as much as possible. 2006-09-02 Robby Stephenson * Fixed bug with CDDB not working (always using debug for Pink Floyd!) 2006-09-01 Robby Stephenson * Fixed bug with completion not working in filter dialog. * Fixed bug with template image gradients not getting updated properly. 2006-08-31 Robby Stephenson * Released Tellico 1.2. 2006-08-30 Robby Stephenson * Fixed crashing bug when using right-click in list view. 2006-08-16 Robby Stephenson * Added Abstract and Keywords to default fields for bibliographies. 2006-08-15 Robby Stephenson * Released Tellico 1.2-pre3. 2006-08-09 Robby Stephenson * Released Tellico 1.2-pre2. 2006-08-07 Robby Stephenson * Fixed bug with merging fields in CSV importer. 2006-08-06 Robby Stephenson * Released Tellico 1.2-pre1. 2006-08-06 Robby Stephenson * Added "next" and "prev" buttons to entry editor. 2006-08-04 Robby Stephenson * Changed HTML export to use libxml2 instead of KHTML for parsing. That should fix the "all text on one line" problem. * Bumped minimum libxml2 verstion to 2.6.0 or greater. 2006-07-30 Robby Stephenson * Added bundled scripts for allocine.fr, Spanish Minstry of Culture, and Dark Horse Comics, all from Mathias Monnerville. 2005-11-13 Robby Stephenson * Bumped minimum KDE version to 3.3. 2006-07-18 Robby Stephenson * Added illustrator field to MODS translator. 2006-07-13 Robby Stephenson * Changed document saving to remember what format (Zip or XML) was opened, and save the file in the same format. 2006-07-04 Robby Stephenson * Updated sorting in HTML export to be locale-aware. * Added auto-completion to filter dialog. 2006-06-30 Robby Stephenson * Improved ISBN-13 compliance. 2006-06-24 Robby Stephenson * Fixed bug with relative url links in template. 2006-06-22 Robby Stephenson * Fixed memory leak in image handling. 2006-06-16 Robby Stephenson * Changed IMDb and Amazon to always use iso-8859-1 encoding for URL. 2006-06-12 Robby Stephenson * Fixed bug with HTML export and MSIE. 2006-06-11 Robby Stephenson * Added date stamp to all report templates. 2006-06-07 Robby Stephenson * Fixed IMDb bug that hid partial name matches for duplicate names. * Implemented workaround for NetAccess::download() that doesn't show download progress info. 2006-06-03 Robby Stephenson * Added validator for unmodified CueCat bar code reader. 2006-05-07 Robby Stephenson * Released Tellico 1.1.6. 2006-05-04 Regis Boudin * Added French documentation. 2006-05-04 Robby Stephenson * Fixed bug that prevented some images from being written to data directory. 2006-04-22 Robby Stephenson * Add font and color config to templates. 2006-04-19 Robby Stephenson * Fixed bug with multiple ISBN search for z29.50. * Fixed "disappearing paragraph" bug in Fancy template. * Fixed name order for IBS searches. * Released Tellico 1.1.5. 2006-04-12 Robby Stephenson * Added Xbox 360, Revolution and Playstation3 to game collection. 2006-04-11 Robby Stephenson * Added view config saving for custom collections based on URL. * Fixed crashing bug when deleting and re-adding fields by same name. * Fixed bug with HTML decoding in IMDb plot summary. 2006-04-10 Robby Stephenson * Fixed bug that re-added default fields when adding search results. 2006-04-07 Robby Stephenson * Added OS check (linux only) for enabling cd-text support. 2006-04-06 Robby Stephenson * Updated Spanish, thanks to Alejandro Hamann and Quique. * Fixed bug with writing temporary files, even when using app data dir. 2006-04-03 Robby Stephenson * Improved PubMed fetcher to grab URL links. 2006-04-02 Robby Stephenson * Changed grouping list to be sorted alphabetically. * Fixed OpenOffice.org plugin build for OOo SDK 2.0.2. * Released Tellico 1.1.4. 2006-03-29 Robby Stephenson * Added referrer to IBS fetcher so images get downloaded. 2006-03-27 Robby Stephenson * Changed XSL templates to output valid HTML 4.01 Strict, patch from Karl Ove Hufthammer. 2006-03-25 Robby Stephenson * Added button for installing new templates. 2006-03-24 Robby Stephenson * Added context menu item in table widget for clearing table. 2006-03-23 Robby Stephenson * Added template preview button in configuration dialog. * Added field for relative folder path to file catalog listing. * Fixed bug with incorrectly setting document modified when loading images. * Fixed bug with loading loans for entries with non-consecutive ids. * Fixed bug that prevented XSLT files with spaces or other non-encoded characters from being opened. * Fixed bug that caused crashes when changing grouping options for some fields. 2006-03-15 Robby Stephenson * Fixed bug that prevented files saved with versions < 0.8 from being opened. 2006-03-12 Robby Stephenson * Released Tellico 1.1.3. 2006-03-10 Robby Stephenson * Fixed bug with comic book template not showing up in a couple of places in the GUI. * Updated MARC2MODS3 stylesheet to MODS 3.1 from LoC. * Added preset z39.50 server list. 2006-03-09 Robby Stephenson * Added isbndb.com fetcher. * Fixed z39.50 search to use proper encoding for search terms. 2006-03-07 Robby Stephenson * Changed ISO 5426 converter to grab diaeresis instead of umlaut. 2006-03-05 Robby Stephenson * Added patch from Karl Ove Hufthammer, for lang-specific sorting. * Released Tellico 1.1.2. 2006-03-04 Robby Stephenson * Fixed file save permissions to remember group. * Fixed backup file permissions. 2006-03-02 Robby Stephenson * Fixed mess with shared pointers holding shared pointers holding shared pointers... * Fixed button size in HTML export. 2006-02-28 Robby Stephenson * Updated fr.po, thanks to Mathias Monnerville. * Fixed GCfilms importer to read running time correctly. * Use IMDb url for updating if available. 2006-02-27 Robby Stephenson * Added pt_BR, thanks to Claudio Felix. 2006-02-23 Robby Stephenson * Added dialog for importing new data sources. 2006-02-22 Robby Stephenson * Fixed HTML export to always use relative links. * Fixed Fancy template column widths, patch from Karl Ove Hufthammer. 2006-02-20 Robby Stephenson * Add Ant Movie Catalog file importer. 2006-02-19 Robby Stephenson * Allow data sources to overwrite current data when updating. 2006-02-18 Robby Stephenson * Added dialog to choose from multiple CDDB lookup responses. * Fields can have default values. * Changed default IMDB server to akas.imdb.com. 2006-02-18 Robby Stephenson * Fixed cleanup when oowriter is closed. * Fixed character encoding for IMDB searches. * Released Tellico 1.1.1. * Branched Tellico 1.1.x. 2006-02-17 Robby Stephenson * Fixed image linking in HTML export. * Updated admin/ and Makefile.am for recent KDE updates. * Changed installation directory of .desktop file to use xdg_appsdir. 2006-02-14 Robby Stephenson * Fixed templates to scale images down to max of 150x200. 2006-02-08 Robby Stephenson * Fixed bug with Video template not showing nationality. * Added ISO 5426 and ISO 6937 character set converters, adapted from MARC4J; 2006-02-07 Robby Stephenson * Dual-licensed the documentation under the GFDL and the FreeBSD Documentation License. * Released Tellico 1.1. 2006-02-04 Robby Stephenson * Added track length to CDDB lookup result. 2006-02-01 Robby Stephenson * Fixed bug with not deleting some temporary files. 2006-01-29 Robby Stephenson * Updated bibtex-translation.xml, thanks to Norbert Nemec. * Updated es.po, thanks to Alejandro Hamann. 2006-01-25 Robby Stephenson * Relaxed MODS importer to look for publisher, isbn, or lccn to determine the entry is a book. Previously, a typeOfResource="text" was required. * Allowed z39.50 source to ask for MODS directly. * Added message box for stderr output from scripts. * Added simple GRS-1 importer for z39.50 fetcher. 2006-01-24 Robby Stephenson * Fixed bug with potential data los for saving image files after changing config option. * Released Tellico 1.1pre3. 2006-01-22 Robby Stephenson * Released Tellico 1.1pre2. * Improved entry views with some graphics, a la amaroK. 2006-01-21 Robby Stephenson * Added drag-n-drop from Mozilla Firefox to image widget. * Changed default behavior to include images in data file, and added a config option. * Added a message box warning about performance when the user first opens a file with more than 100 images. 2006-01-20 Robby Stephenson * Fixed bug with parsing arguments to external data source scripts. 2006-01-19 Robby Stephenson * AnimneNfo.com source added by default, and IBS.it added if Italian is in the user's language list. * Alexandria exporter includes publishing year now. 2006-01-17 Robby Stephenson * Alexandria importer includes publishing year now. 2006-01-15 Robby Stephenson * Changed file saving to be smart about which images need written to cache. Should improve save-time. * Fixed bug with undo/redo not correctly reverting to old values. 2006-01-14 Robby Stephenson * Added Russian translation, from Artur Kalimullin. 2006-01-12 Robby Stephenson * Released Tellico 1.1pre1. * Added/updated SRU fetcher. 2006-01-11 Robby Stephenson * Added Internet Bookshop (ibs.it) book source. 2006-01-10 Robby Stephenson * Added AnimeNfo.com search source. 2006-01-09 Robby Stephenson * Fixed Debian bug #346414 in hu.po. 2006-01-08 Robby Stephenson * Changed image loader to read and write images inside the Tellico shared directory to speed up loading, etc. 2005-12-06 Robby Stephenson * Merged OpenOffice.org interop. Requires SDK for compilation. 2006-01-05 Robby Stephenson * Added Yahoo! Album Search source. * Added Tri-Column video report template. 2005-12-15 Robby Stephenson * Expand external app fetcher to include any of the fetch keys. 2005-12-08 Robby Stephenson * Increased max table columns to 10. 2005-12-07 Robby Stephenson * Updated Amazon fetcher to allow selecting keywords or not. 2005-12-05 Robby Stephenson * Changed bibtex behavior to not add braces around capital letters. 2005-11-29 Robby Stephenson * Moved no-capitalization list to config dialog. 2005-11-28 Robby Stephenson * Updated Hungarian, from Csaba Zakarias. 2005-11-17 Robby Stephenson * Added separate UPC searching for Amazon. 2005-11-16 Robby Stephenson * Added CD-Text reading to FreeDBImporter, plus option to disable compilation in case of platform issues. * Move to Amazon ECS version 4. 2005-11-15 Robby Stephenson * Added IMDB rating field to fetcher. 2005-11-13 Robby Stephenson * Bumped minimum KDE version to 3.2 and minimum QT version to 3.2. 2005-11-10 Robby Stephenson * Custom data source fields are now configurable. 2005-11-07 Robby Stephenson * Made size of IMDb cast list configurable. * Broke out cite actions into submenu, and added copying bibtex to clipboard. 2005-11-03 Robby Stephenson * Bumped XML syntax version to 9 to include file catalog and changing default music collection track field to 3 columns. 2005-11-01 Robby Stephenson * Fixed compilation on GNU/kFreeBSD, Debian bug #336949, patch from Aurelien Jarno. 2005-10-29 Robby Stephenson * Improved table fields to show column titles. * Changed buttons to a right-click popup. 2005-10-27 Robby Stephenson * Improved audio file metadata importer to include track length as an additional column in the track field. 2005-10-26 Robby Stephenson * The audio file importer now checks for track numbers in the file name, patch from Andras Mantia. 2005-10-19 Robby Stephenson * Added a string store to reduce memory consumption. 2005-10-17 Robby Stephenson * Released Tellico 1.0.3. 2005-10-15 Robby Stephenson * Added buttons for inserting and removing rows from a table. 2005-10-13 Robby Stephenson * Enabled auto-updating for certain entries and data sources. 2005-10-12 Robby Stephenson * Updated it.po, from Lorenzo Novaro. 2005-10-11 Robby Stephenson * New and modified entries now show labels indicating so. 2005-10-08 Robby Stephenson * Improved status bar. 2005-10-07 Robby Stephenson * Updated CalendarHandler to compile with KDEPIM 3.5. 2005-10-06 Robby Stephenson * Fixed bug with bibtex macro names always being imported in lowercase. 2005-10-03 Robby Stephenson * Fixed bug with initialization, patch from Marco Clemencic. 2005-10-02 Robby Stephenson * Released Tellico 1.0.2. * Updated IMDB fetcher. 2005-09-25 Robby Stephenson * Fixed bug with auto-detecting MARC encoding for z39.50 import. 2005-09-22 Robby Stephenson * Fixed bug with CSV importer not properly parsing some non-CSV files. * Fixed bug with entry selection in icon view. * Added GCfilms exporter; 2005-09-21 Robby Stephenson * Fixed bug with showing invalid loan due date. * Added GCfilms importer. * Added IMDB link when fetching info from IMDB. * limited number of cast results form IMDB to 20; 2005-09-20 Robby Stephenson * Released Tellico 1.0.1. 2005-09-19 Robby Stephenson * Fixed bug with loading utf-8 encoded bibtex files. 2005-09-18 Robby Stephenson * Fixed bug with variable icon sizes in config dialog. 2005-09-16 Robby Stephenson * Fixed bug for merging field properties, which mostly showed up as double-colons in music collection tracks. * Fixed crashing bug with modifying filters and cancelling. 2005-09-11 Robby Stephenson * Added Norwegian Nynorsk translation from Karl Ove Hufthammer. 2005-09-10 Robby Stephenson * Fixed bug in audio file metadata import that was overwriting track listings incorrectly. * Fixed crashing bug for selecting groups and entries in group view. 2005-09-09 Robby Stephenson * Released Tellico 1.0. 2005-09-05 Robby Stephenson * Added Polish translaion from Marek Janukowicz. 2005-08-22 Robby Stephenson * Fixed bug with external application fetcher not reading path entries correctly. * Updated Spanish translation, thanks to Brian Hughes. 2005-08-21 Robby Stephenson * Updated Swedish, thanks to Peter Landgren. * Added patch from Nix to dynamically resize table columns. 2005-08-20 Robby Stephenson * Released Tellico 1.0pre2. 2005-08-14 Robby Stephenson * Changed audio file metadata to ignore case when doing album comparisons. * Changed collection loader to better compensate for old rating fields. * Changed AddEntry command to act on multiple entries. 2005-08-11 Robby Stephenson * Updated Dutch translation, thanks to Fred Marchee. * Updated Fininsh translation, thanks to Teuvo Eloranta. 2005-08-06 Robby Stephenson * Released Tellico 1.0pre1. 2005-08-04 Robby Stephenson * When exporting HTML, copy any relative images, etc. 2005-08-01 Robby Stephenson * Strip HTML tags from paragraph export to PilotDB. 2005-07-30 Robby Stephenson * Make the amazon importer default to medium images instead of none. 2005-07-28 Robby Stephenson * Make the 'Fancy' template default. 2005-07-01 Robby Stephenson * Released Tellico 0.13.8. 2005-06-30 Robby Stephenson * Fixed bug with CSV import parsing. 2005-06-28 Robby Stephenson * Added license exception for linking to OpenSSL. 2005-06-25 Robby Stephenson * Added a status message timeout for main window and fetch dialog. 2005-06-23 Robby Stephenson * Fixed crashing bug for fields with empty categories. 2005-06-22 Robby Stephenson * Added initial Catalan translation from David Majà Martínez. 2005-06-20 Robby Stephenson * Added fetcher for reading output from an external application. * Fixed bug with wrong icon size for entry action. 2005-06-17 Robby Stephenson * Added video game collection type. 2005-06-12 Robby Stephenson * Added capability for recursively reading all CDDB cache files, based on patch from Marco Hofmann. 2005-06-08 Robby Stephenson * Fixed bug with file extension defaulting to .bc. 2005-06-02 Robby Stephenson * Changed Table field to allow up to 5 columns. Deprecated Table2 type. 2005-06-01 Robby Stephenson * Added Rating field type. 2005-05-31 Robby Stephenson * Fixed bug with grouping by checkbox fields. 2005-05-30 Robby Stephenson * Updated Norwegian translation. * Added Unimarc support to z39.50 importer. 2005-05-25 Robby Stephenson * Released Tellico 0.13.7. 2005-05-25 Robby Stephenson * Changed bibtex exporter to put crossref'd entries at the end. Only one level of crossref is allowed. 2005-05-19 Robby Stephenson * Fixed bug with changing between rating and normal choice field. 2005-05-18 Robby Stephenson * Updated Dutch translation, thanks to Fred Marchee. 2005-05-13 Robby Stephenson * Fixed bug with clearing table fields. * Added ONIX export, borrowing format from Alexandria. 2005-05-10 Robby Stephenson * Changed contact email to tellico-users mailing list. 2005-04-27 Robby Stephenson * Fixed corner case xml element naming for new fields. * Fixed typo on allowed and description in fields editor. 2005-04-10 Robby Stephenson * Fixed bug with having fields named after xml elements, like entry or field. 2005-04-09 Robby Stephenson * Added a PubMed search source, thanks to an XSL stylesheet from Michaël Zugaro. 2005-04-08 Robby Stephenson * Added some accelerators, thanks to patch from Felix Berger. 2005-04-07 Robby Stephenson * Added Undo/Redo framework. 2005-04-03 Robby Stephenson * Released Tellico 0.13.6. 2005-03-29 Robby Stephenson * Fixed bug with adding multiple entries from fetch dialog not added additional images. * Added Portuguese translation, thanks to Ligia Moreira. 2005-03-23 Robby Stephenson * Changed internet search dialog to clear results when running a new search. * Changed selection so that hidden items are not selected. 2005-03-16 Robby Stephenson * Added shortcuts for entry grouping and filter. 2005-03-08 Robby Stephenson * Released Tellico 0.13.5. 2005-03-02 Robby Stephenson * Updated German translation, thanks to Gerrit Albrecht. 2005-03-01 Robby Stephenson * Fixed crash for some amazon searches. * Fixed character encoding for Japanese amazon searches. 2005-02-27 Robby Stephenson * Released Tellico 0.13.4 2005-02-25 Robby Stephenson * Added a configure param to disable cddb even if kcddb headers found. 2005-02-24 Robby Stephenson * Updated bibtex translation map, fixing some bugs. * Fixed bug with Amazon search character encoding. * Changed Bibtex handler to convert {MIT} to MIT, importing and exporting. 2005-02-23 Robby Stephenson * Fixed bug with grouping being enabled for paragraphs. 2005-02-17 Robby Stephenson * Fixed bug with exporting empty groups in HTML. * Added capability for adding loan due notice to KOrganizer calendar. 2005-02-14 Robby Stephenson * Fixed bug with RIS importer messing up entry types. 2005-02-10 Robby Stephenson * Released Tellico 0.13.3. 2005-02-08 Robby Stephenson * Fixed bug in configure script for disabling amazon, thanks to Markus Brueffer. * Fixed bug with spin boxes not enabling Apply button in Config Dialog. * Fixed bug with audio file metadata import for compilations. 2005-02-05 Robby Stephenson * Update some bibtex character translations. * Released Tellico 0.13.2. 2005-02-03 Robby Stephenson * Updated IMDb fetcher. * Updated bibtex import and export to recognize {X} constructions. * Fixed bug with locale encoding for HTML export. * Changed statusbar to show tooltips for menu items. 2005-02-02 Robby Stephenson * Added France and Canada locales to Amazon.com sources. 2005-02-01 Robby Stephenson * Fixed bug with importing RIS with no final space. * Fixed Debian bug #290467 for gcc 4.0 compilation, thanks to Regis Boudin. 2005-01-28 Robby Stephenson * Removed find dialog since the filter works so much better. 2005-01-27 Robby Stephenson * Removed option for toggling entry count show in group view. * Added check-out for lending items with view. 2005-01-25 Robby Stephenson * Fixed Debian bug #292165 with bibtex export of umlauts. 2005-01-21 Robby Stephenson * Updated Swedish translation, thanks to Peter Landgren. 2005-01-20 Robby Stephenson * Added saved filters with view. * Changed IMDb cast page to decode HTML entities, thanks to Rafa Kortes. 2005-01-12 Robby Stephenson * Changed double click in the Internet Search dialog to add the entry. 2005-01-11 Robby Stephenson * Added collection report dialog. 2005-01-06 Robby Stephenson * Improved file loading efficiency somewhat. 2005-01-05 Robby Stephenson * Changed image loading to be on-demand for local zip files. 2004-12-19 Robby Stephenson * Added DCOP interface for exporting files. * Added vertical line between columns in list view. * Translated xsl entry template names. 2004-12-18 Robby Stephenson * Added DCOP interface for importing files. 2004-12-17 Robby Stephenson * Branched to 0.13.x. * Updated audio file importer to use "(Various)" for multi-artist albums. 2004-12-11 Robby Stephenson * Fixed a bug in PilotDB exporting, I think. * Added PilotDB export support for Date fields. * Released Tellico 0.13.1. 2004-12-10 Robby Stephenson * Fixed some date formatting. 2004-12-09 Robby Stephenson * Fixed auto bibtex key generation for empty keys. 2004-12-08 Robby Stephenson * Fixed Entry View to react to changes in the color palette. 2004-12-06 Robby Stephenson * Increased maz z39.50 port to 999999. * Fixed some KMAX, KMIN template issues with casts. * Added column shading for detailed view, per KDE bug 59791. 2004-12-03 Robby Stephenson * Updated Finnish translation, thanks to Tuevo Eloranta. * Updated Norwegian translation, thanks to Leif Mathis Gaup. * Fixed *BSD compile, with patch from Markus Brüffer. 2004-12-01 Robby Stephenson * Enabled relative URLs in URL fields. * Disabled running executables in URL fields. * Released Tellico 0.13. 2004-11-30 Robby Stephenson * Removed all the KStaticDeleter references. Don't get fancy. * Updated title sorts to work for articles ending with an apostrophe. * Updated audio file metadata importer to recognize albums with multiple artists and change track list accordingly. * Fixed CSV importer to properly find pre-existing, non-default fields. 2004-11-25 Robby Stephenson * Added stars for up to a 10 rating. * Added checkmark in entry templates for bool fields. 2004-11-20 Robby Stephenson * Added Spanish tips translation from ventolera. 2004-11-19 Robby Stephenson * Released Tellico 0.13pre3. 2004-11-18 Robby Stephenson * Added command-line options for loading bibtex, MODS, and RIS files. 2004-11-17 Robby Stephenson * Updated Amazon fetcher to work with more than 10 ISBN. * Updated z39.50 fetcher to work with more than 10 ISBN. * Added language lookup table to mods2tellico.xsl. * Updated lyxpipe cite to auto-generate bibtex key if necessary. * Added "load ISBN list from file" to search dialog. 2004-11-14 Robby Stephenson * Released Tellico 0.13pre2. 2004-11-12 Robby Stephenson * Improved printing speed. * Added user and password config for z39.50. * Added address field to default bibliography. 2004-11-10 Robby Stephenson * Updated French translation, thanks to Regis Boudin. * Fixed layout bug in config dialog. 2004-11-09 Robby Stephenson * Added fix similar to kde bug 86188 for including linux/cdrom.h. * Changed latex cite to separate multiple cites with ", " instead of "," to better match pybliographer. 2004-11-08 Robby Stephenson * Added host config to IMDB. * Added image download option to Amazon and IMDB. * Updated kde-common/admin/cvs.sh to allow other automake versions. * Released Tellico 0.13pre1. 2004-11-07 Robby Stephenson * Added character set config to z39.50. * Changed main document to tellico.xml. 2004-10-31 Robby Stephenson * Updated initial minimum width to 600 pixels. * Added group selection entry to collection menu. * Updated Find Dialog to remove FromBeginning (didn't make sense) 2004-10-23 Robby Stephenson * Updated HTML export and printing stylesheets a bit. 2004-10-18 Robby Stephenson * Added patch to compile on g++ 2.95 from Markus Brueffer. 2004-10-11 Robby Stephenson * Added patch from wwp to fix saving file on quit. 2004-10-02 Robby Stephenson * Added confirm message box when deleting an entry. 2004-09-23 Robby Stephenson * Changed default file textension to .tc. 2004-09-15 Robby Stephenson * Added Unrated certification to default video collection. 2004-09-14 Robby Stephenson * Renamed Bookcase to Tellico * Added IMDB fetcher. * Incremented DTD version to 7, changed root element to tellico. * Changed zip file to use maindoc.xml as do the KOffice files. 2004-09-10 Robby Stephenson * Added new icons from Virginie Quesnay. 2004-09-09 Robby Stephenson * Updated French translation, thanks to Regis Boudin. * Removed libcdda (cdparanoia) dependency, using cd-discid code instead. * Added rating widget in entry editor, using stars. 2004-09-07 Robby Stephenson * Really fixed gcc 2.95 compilation. * Added RIS importer. 2004-09-02 Robby Stephenson * Released Bookcase 0.11. 2004-09-01 Robby Stephenson * Added FreeDB importer. 2004-08-27 Robby Stephenson * Fixed compile on gcc 2.95. 2004-08-26 Robby Stephenson * Added Alexandria importer. * Added Alexandria exporter. 2004-08-25 Robby Stephenson * Updated Norwegian translation, thanks to Leif Mathis Gaup. * Read album covers from .directory files in audio file importer. 2004-08-24 Robby Stephenson * Added support for reading audio file meta-data. 2004-08-22 Robby Stephenson * Updated paragraph fields to remember line-breaks. * Released Bookcase 0.10. 2004-08-20 Robby Stephenson * Added HTML formatting to paragraphs in entry templates. * Updated German translation, thanks to Gerrit M. Albrecht. 2004-08-18 Robby Stephenson * Added Latin1Literal class to speed up string comparisons. * Added MODS import menu item. 2004-08-10 Robby Stephenson * Added "export selected entries only" option. 2004-08-09 Robby Stephenson * Fixed UI file location. * Released Bookcase 0.10pre2. 2004-08-08 Robby Stephenson * Released Bookcase 0.10pre1. * Added entry icon to fetch dialog button. 2004-08-03 Robby Stephenson * Fixed bug for exporting HTML for entries with multiple titles. * Changed HTML export to put entry files in separate directory. * Added link to parent collection file for entry templates. 2004-08-01 Robby Stephenson * Updated French translation, thanks to Rui Nibau. * Changed to show a spinbox for single number values; 2004-07-30 Robby Stephenson * Fixed bug with capitalization of words split by dash or comma, and ending with apostrophes. 2004-07-13 Robby Stephenson * Fixed bugs with enabling apply/ok buttons in fields dialog for extended properties, and disappearing groups when renaming fields. 2004-07-02 Robby Stephenson * Added collection and group view in stack. 2004-06-26 Robby Stephenson * Fixed bug with copying selected entries. 2004-06-21 Robby Stephenson * Fixed gcc 3.4 compilation bug in src/translators/btparse/err.h 2004-06-07 Robby Stephenson * Update Norwegian translation, thanks to Leif Mathis Gaup. 2004-05-27 Robby Stephenson * Released Bookcase 0.9.3. * Updated italian translation, thanks to FaUsT. 2004-05-25 Robby Stephenson * Fixed crashing bug for exporting when no fields can be grouped. 2004-05-24 Robby Stephenson * Restructured ISBN code to get rid of some old cruft. * Added patch from Regis Boudin to add French ISBN formatting and remove old copyright restricted ISBN code. 2004-05-22 Robby Stephenson * Added entry group templates. 2004-05-19 Robby Stephenson * Changed focus to move to next entry when one is deleted. * Changed document loader to allow i18n attribute indicate translation. * Fixed bug with empty group names. 2004-05-17 Robby Stephenson * Released Bookcase 0.9.2. 2004-05-15 Robby Stephenson * Fixed bug with xslt import with libxml < 2.6. * Fixed bug with losing extended properties when cloning fields/ 2004-05-10 Robby Stephenson * Added Amazon fetcher. * Added SRU Fetcher. 2004-05-07 Robby Stephenson * Fixed bug with XSLT importer. * Added MODS XSLT importer file. 2004-05-06 Robby Stephenson * Branched 0.9.x 2004-05-02 Robby Stephenson * Changed Fields Dialog to close when creating a new collection or opening one. * Removed GCJ/java checks from configure. * Released Bookcase 0.9.1 2004-04-30 Robby Stephenson * Fixed i18n extraction for UI and gettext extensions. * Removed Japanese translation because of encoding errors. * Updated CSV import to know about current fields when appending or merging 2004-04-29 Robby Stephenson * Updated German translation from Gerrit Albrecht. 2004-04-27 Robby Stephenson * Fixed bug with lyxpipe using LYXSRV insted of LYXCMD. * Changed quick filter to wait 200 ms before updating the filter. 2004-04-21 Robby Stephenson * Added wait cursor for file saving. * Fixed bug with created a double collection when failing to load the initial file. * Make sure column view updates when an image is cleared. 2004-04-18 Robby Stephenson * Released Bookcase 0.9. 2004-04-16 Robby Stephenson * Improved visibilty of image size in Fancy.xsl. * Updated Italian translation, thanks to FaUsT. * Updated Finnish translation, thanks to Teuvo. * Added hidden pref for pixmap size in detailed view. * Changed HTML exporter to use titles as filenames. 2004-04-13 Robby Stephenson * Added hidden preference for pilotdb export charset encoding. 2004-04-09 Robby Stephenson * Improved bibtex export to detect duplicates keys and add 'a'b, 'b', etc. * Released Bookcase 0.9pre1 2004-03-26 Robby Stephenson * Updated printing and HTML export to include images. * Updated Detailed View to show images, too. 2004-03-25 Robby Stephenson * Updated Italian translation, thanks to FaUsT. * Changed toolbar icon for New Entry to collection icon on top of empty mime type. 2004-03-24 Robby Stephenson * Added menu item in group view for filtering on groups. * Added config options for image sizes in printout. 2004-03-22 Robby Stephenson * Fixed another bug with HTML export character encoding. * Added config panel for bibtex quotation style and lyxpipe location 2004-03-20 Robby Stephenson * Fixed Paragraph fields to properly signal modified in the Entry Editor. * Fixed crashing bug for fields with nothing but whitespace in the datafile. * Updated printing stylesheet so that images can be included. 2004-03-19 Robby Stephenson * Changed document opening so that if the first field has the name of "_default", all the default fields will be added. 2004-03-17 Robby Stephenson * Changed Dependent fields to allow grouping. * Changed Fields Dialog to allow changing the field types, in some limited cases. 2004-03-15 Robby Stephenson * Fixed crashing bug in PilotDB export with images. * Fixed bug with "other" delimiter freezing the app. 2004-03-13 Robby Stephenson * Fixed bug with trying to reopen Untitled after converting to a bibliography. * Added support for lyxpipe and citing entries in bibliography. 2004-03-12 Robby Stephenson * Changed to be more aggressive in creating URL fields on Bibtex import. 2004-03-10 Robby Stephenson * Added Compact and Fancy entry templates. * Released Bookcase 0.8.5. 2004-03-07 Robby Stephenson * Fixed bug in Video template for when there's no Cast template. * Fixed bug with detecting old versions of gcc and vector::at. 2004-03-05 Robby Stephenson * Fixed bug with filter dialog layout and KDE 3.2. 2004-03-04 Robby Stephenson * Fixed bug with corrupt zip files when more than one entry references the same image. 2004-02-28 Robby Stephenson * Fixed the gcc 2.95 compile bug for sure. 2004-02-25 Robby Stephenson * Fixed crashing bug in Data::Entry::field(). * Fixed XSLTHandler to work for URLs, so XSLT files can reference other files. 2004-02-22 Robby Stephenson * Fixed bug with HTML entities in XSLT stylesheet params. 2004-02-20 Robby Stephenson * Fixed bug with duplicate menu items in KDE 3.2.x. * Fixed bug to properly reorder categories when reordering fields. * Released Bookcase 0.8.4. 2004-02-19 Robby Stephenson * Fixed auto-capitalize and auto-format to take affect immediately. 2004-02-18 Robby Stephenson * Added Czech translation, thanks to Robert Kratky. 2004-02-17 Robby Stephenson * Fixed compilation issues for gcc 2.95 and FreeBSD. * Changed the Config Dialog to use a semi-colon to separated the articles, prefixes, and suffixes instead of a comma, for consistency. They're still saved in the config file with a comma, though, so old options should be fine. 2004-02-14 Robby Stephenson * Fixed problem when trying to reopen last file and getting error. * Added drag & drop support for the image widget. 2004-02-13 Robby Stephenson * Updated Estonian translation, from Toomas Nigola. * Changed to remember sorting config in group view. 2004-02-12 Robby Stephenson * Fixed crashing bug when renaming a single-category field. 2004-02-11 Robby Stephenson * Changed Entry Editor to redo page layout when deleting fields. * Added merge functionality when importing collections. * Fixed bug with exporting HTML in non-utf8. 2004-02-09 Robby Stephenson * Updated Spanish translation, thanks to Quique. * Added sorting by group count. 2004-02-07 Robby Stephenson * Added workaround for layout bug in Keramik style. 2004-02-04 Robby Stephenson * Fixed saving visible columns yet again. * Changed XSLT parser to prevent all network calls. * Fixed crashing bug when deleting a field. * Fixed compilation bug when using libxml > 2.6.0 but libxslt < 1.1. * Released Bookcase 0.8.3. 2004-02-04 Robby Stephenson * Fixed file truncation for file with some Unicode characters. * Released Bookcase 0.8.2. 2004-02-03 Robby Stephenson * Released Bookcase 0.8.1. 2004-02-02 Robby Stephenson * Added Album entry template. * Added Finnish translation, thanks to Teuvo Eloranta. * Added --with-xml-catalog configure option. 2004-01-30 Robby Stephenson * Fixed compile bug with src/Makefile.am and libxslt. * Fixed bug with $datadir param in Default entry template. * Added Video entry template. * Added EAN to ISBN conversion for 978 and 979 codes. Patch from Martijn Pieters. 2004-01-27 Robby Stephenson * Fixed bug with Select All not working. * Fixed some compilation issues. * Updated French translation from Rui Nibau. 2004-01-25 Robby Stephenson * Fixed DTD for new collections. * Updated some accel keys. * Changed Requires to KDE 3.1 or higher since KZip is used. * Released Bookcase 0.8. 2004-01-23 Robby Stephenson * Released Bookcase 0.8pre1 2004-01-22 Robby Stephenson * Added Finnish translation from Teuvo Eloranta. * Added "Export to XML" option since file format is Zip now. * Switched to new XML parser in libxml2 > 2.6.0 2004-01-13 Robby Stephenson * Added new Collection menu. 2004-01-09 Robby Stephenson * Changed entry method for bibtex string macros. Triple-clicking on list view items is stupid. 2004-01-08 Robby Stephenson * Added zip file format for collections with images. 2004-01-01 Robby Stephenson * Added cut, copy, and paste functions. * Added new entry and copy entry functions. * Added find previous, select all, and deselection functions. 2003-12-31 Robby Stephenson * Changed double clicking an entry to show the editor. * Moved entry edit widget to dialog box. 2003-12-13 Robby Stephenson * Updated French translation, thanks to RNB. 2003-12-11 Robby Stephenson * Added image fields. * Changed auto-completion to account for fields with multiple values. 2003-12-07 Robby Stephenson * Updated Norwegian translation, thanks to Leif Mathis Gaup. 2003-11-25 Robby Stephenson * Fixed bug in remembering visible columns for custom fields. * Added some speed-ups in loading time. * Released Bookcase 0.7.2 2003-11-24 Robby Stephenson * Fixed another ISBN format bug for non-english publishers. 2003-11-23 Robby Stephenson * Fixed bug with auto-capitalization when global setting was turned off. * Fixed bug with not saving options for empty collections. * Fixed bug with reordering visible fields. 2003-11-21 Robby Stephenson * Added Pilot-DB export option. * Fixed bug to update status line after deleting entries. 2003-11-12 Robby Stephenson * Added Derived field type for concatenating values from other fields. * Added default collections for trading cards, coins, stamps, comic books, and wines. 2003-11-11 Robby Stephenson * Updated German tranlation, thanks to Gerrit Albrecht. * Added Swedish translation, thanks to Karolina Lindqvist. * Fixed bug with adding 2-column table. * Cut the 0.7.x branch. * Added Derived field type. 2003-11-09 Robby Stephenson * Fixed charset encoding bug. * Fixed compile for GCC 2.96. * Changed bibtex and CSV export to default to locale encoding. * Fixed bug with field editor not updating title. * Released Bookcase 0.7.1. 2003-11-08 Robby Stephenson * Released Bookcase 0.7. 2003-11-05 Robby Stephenson * Fixed the tips file translation issue. * Released Bookcase 0.7pre2. 2003-11-03 Robby Stephenson * Fixed ISBN formatting bug for non-english language publishers. * Released Bookcase 0.7pre1. 2003-10-25 Robby Stephenson * Fixed documentation compile error for KDE 3.2. * Added CSV importer. * Changed Bibtexml importer and exporter to use internal classes rather than XSL stylesheets. 2003-10-24 Robby Stephenson * Added string macro editor for Bibtex collections. * Printing now only prints visible entries by default. 2003-10-21 Robby Stephenson * Added new group for all attributes formatted as names, so Editors and Authors can be grouped together. 2003-10-10 Robby Stephenson * Changed printing to print columns as shown in the view, and group by the current grouping. * Removed old options for printing fields selection. 2003-10-04 Robby Stephenson * Added capability to reorder fields in field editor. 2003-10-03 Robby Stephenson * Imported btparse library code to use for importing Bibtex files. It has a lot of warnings, but none should be fatal. 2003-09-23 Robby Stephenson * Added "Bibtex Field" to default Bibtex collections fields to aid in exporting to Bibtex and Bibtexml. * Bumped document syntax to 4. * Changed "attribute" elements in document file to "field". * Changed all unit elements to "entry" rather than the unit name. * Changed Boolean values to save as "true" instead of an implicit value. * Added CSV exporter. 2003-09-12 Robby Stephenson * Added 2-column Table field type. 2003-09-09 Robby Stephenson * Changed boolean values to show title in group view. 2003-09-03 Robby Stephenson * Fixed bug when deleting multiple books from detailed listview. 2003-08-31 Robby Stephenson * Changed field completion to be case-insensitive. * Fixed bug with saving listview columns. 2003-08-16 Robby Stephenson * Fixed bug with reopening last saved file. 2003-08-12 Robby Stephenson * Added French translation from RNB. 2003-08-08 Robby Stephenson * Added menu item for configuring toolbars. * Added menu item for configuring shortcut keys. 2003-07-31 Robby Stephenson * Added Table field type. 2003-07-30 Robby Stephenson * Fixed bug in saving visible columns. * Improved loading time. 2003-07-29 Robby Stephenson * Implemented adding file extension (.bc) when using File->Save As and an extension filter is shown. * Added status bar text showing number of selected books when more than 1 is selected. 2003-07-24 Robby Stephenson * Added menu comands for showing/hiding the group view and editor. 2003-07-23 Robby Stephenson * Added "--nofile" command line option for bypassing the "Open last file" config setting. * Added "Tip of the Day" dialog. * Changed default for showing unit count to true. * Changed Year field type to generic Number. * Improved sorting to sort numerically for Number fields. * Added Default button to Field Editor dialog. * Added additional format option to never capitalize or format. * Fixed last open file option to no longer be over-written when Bookcase is exited with an empty document. * Cut the 0.6.x branch. 2003-07-22 Robby Stephenson * Fixed crashing bug in "Edit Fields" dialog. 2003-07-21 Robby Stephenson * Released Bookcase 0.6.5 2003-07-09 Robby Stephenson * Added Estonian translation from Toomas Nigola. 2003-07-07 Robby Stephenson * Updated German translation from Ulf-Diether Ehlert. 2003-07-01 Robby Stephenson * Fixed translation bug with category names. * Added Bulgarian translation from Boyan Ivanov. 2003-06-25 Robby Stephenson * Fixed sorting bug for empty fields. * Released Bookcase 0.6.4 2003-06-19 Robby Stephenson * Changed focus to first tab when adding new book. * Disabled multiple value option for title field in field editor. * Fixed bug with completion object not being updated when a field is changed to all auto-completion. * Fixed drawing bug with toolbar label background. 2003-06-17 Robby Stephenson * Fixed bug with surname matching too much with periods in regexp. * Added Japanese translation from Linux Magazine. * Added Dutch translation from Liese De Vos. 2003-05-30 Robby Stephenson * Fix crashing bug when opening the filter dialog on KDE 3.0. 2003-05-27 Robby Stephenson * Released Bookcase 0.6.3 2003-05-26 Robby Stephenson * Added feature where the column view remembers the previous sorted column, so that items with the same sort key are subsequently sorted by previous column. 2003-05-25 Robby Stephenson * Add Spanish translation, thanks to Quique. * Updated Norwegian translation, thanks to Leif Mathis. * Fix bug with collapse/expand all not working in group view. * Fix bug with "Find" not properly traversing the collection. * Fix bug with sorted column not being saved between document loads. * Fix bug with changed category not being reflected in edit widget. * Fix bug with name prefixes not being limited to word boundaries. 2003-05-11 Robby Stephenson * Released Bookcase 0.6.2 2003-05-10 Robby Stephenson * Fixed bug with custom fields not showing up in column popup menu. * Fixed crashing when selecting a different book after modifying the current one and not saving it. * Fixed icon bug for column view. * Prettied-up some of layout code. 2003-05-09 Robby Stephenson * Added Norwegian translation from Leif Mathis Gaup. * Fixed printing bug for books with multiple groups. 2003-05-06 Robby Stephenson * Changed initial file opening so that if a new empty file was the last viewed, then no recent file is opened at next startup. * Fixed bug with initial collection not updating edit widgets. 2003-05-05 Robby Stephenson * Released Bookcase version 0.6.1 * Fixed automake 1.7 error with configure.in.in * Fixed UIC variable usage, primarily for *BSD platforms * Fixed double encoding bug on bibtexml inport * Fixed wrong namespace on bibtexml export * Changed to use KAcceleratorManager for keyboard accels on tabs * Fixed bug with the group view icon not getting changed back to the folder 2003-05-03 Robby Stephenson * Fixed bug with editing multiple books and getting asked every time if I'm sure. * Changed modified field names in the "Edit Fields" dialog to be colored, an din italic font instead of having an asterisk in front. 2003-05-02 Robby Stephenson * Released Bookcase version 0.6 * Changed Bibtex export to use the Locale character encoding instead of UTF-8 2003-04-22 Robby Stephenson * Added a URL attribute type * Changed articles, surname prefixes, and suffixes to be case-insensitive so the config value no longer has to include both capitalized and lower-case values * Broke out the attribute format flag for cleaner code 2003-04-13 Robby Stephenson * Added multiple selection, batch editing 2003-04-12 Robby Stephenson * Added RegExp Editor button to find dialog * Added Quick and Advanced filtering in the detailed list view 2003-04-10 Robby Stephenson * Added new "person" icon for grouping when the group is a name 2003-04-09 Robby Stephenson * Convert from KTabCtrl to QTabWidget, which has more documentation and is actively developed 2003-04-08 Robby Stephenson * Fixed bug with column order not retained on save 2003-04-05 Robby Stephenson * Fixed bug with validating multiple copyright years 2003-03-25 Robby Stephenson * Added surname prefixes like 'de' and 'von' 2003-03-23 Robby Stephenson * Fixed debug mistake. * Released Bookcase version 0.5.2a 2003-03-22 Robby Stephenson * Released Bookcase version 0.5.2 2003-03-21 Robby Stephenson * Added German translation from Gerrit M. Albrecht * Fixed bugs with empty suffix and article list * Fixed some i18n issues 2003-03-18 Robby Stephenson * Added Hungarian translation from Marcel Hilzinger 2003-03-15 Robby Stephenson * Fixed types and compile problems. Thanks, Dre! * Released Bookcase version 0.5.1 2003-03-14 Robby Stephenson * Put the book count in color. * Took out general export function for now. * Released Bookcase version 0.5 2003-03-07 Robby Stephenson * Added a new printing page in the configuration dialog * Added a checkmark pixmap for the boolean fields * Added a header menu to be able to select which fields to show in the detailed list view 2003-03-02 Robby Stephenson * Fixed character encoding bug for latin2 2003-01-25 Robby Stephenson * Fixed focus bug 2003-01-19 Robby Stephenson * Changed FORMAT_VERSION to 2 * Changed document format so that attributes which allow multiple entries are saved in multiple XML elements * Changed "keywords" attribute to "keyword" * Added check for document format version so that incompatible versions are not loaded. Older versions will be properly loaded, but once saved, previous versions of Bookcase will not load the new files properly. * Changed "Language" and "Copyright Year" to allow multiple entries. 2002-12-12 Robby Stephenson * Added namespaces to document file 2002-12-11 Robby Stephenson * Added import from Bibtexml using XSLT 2002-12-08 Robby Stephenson * Added export to Bibtex using XSLT * Added export to Bibtexml using XSLT 2002-12-03 Robby Stephenson * Released Bookcase version 0.4.1 * Added a Romanian translation from Iulian Ursache 2002-11-29 Robby Stephenson * Fixed compile problems with gcc 2.95 2002-11-24 Robby Stephenson * Released Bookcase version 0.4 * Printing improvements in XSLT layout 2002-11-10 Robby Stephenson * Added an Italian translation from FaUsT 2002-11-05 Robby Stephenson * Added a new toolbar for changing the grouping of the units * Changed the bookcase.spec file to add proper files 2002-10-20 Robby Stephenson * Released Bookcase version 0.3 2002-08-27 Robby Stephenson * Added two new XSLT files * Added printing support using KHTML * Added RPM spec file to automake 2002-02-21 Robby Stephenson * Released Bookcase version 0.2 2002-02-20 Robby Stephenson * Added BCColumnView::slotShowUnit() so to ensure that the proper listview in the stack is shown. * Added BCAttribute::capitalize for auto capitalization of names and titles. 2002-02-08 Robby Stephenson * Changed selection behavior so the two listviews do not sync now. Clicking in one clears the other. 2002-02-03 Robby Stephenson * Added article and suffix list to options in config dialog. 2002-01-11 Robby Stephenson * BCListView is now BCColumnView and can flip between different collections. Woo-hoo! Now multiple collections behave properly. Also started adding configuration options. 2002-01-10 Robby Stephenson * bctabcontrol.{h,cpp} Need KTabCtl::showTab to be public, so subclassed it. 2002-01-10 Robby Stephenson * configdialog.{h,cpp} Added beginnings of configuration dialog. 2001-12-06 Robby Stephenson * bcgroupview.cpp, bcattribute.cpp Fixed the bug for formatting names with suffixes like Jr. 2001-11-05 Robby Stephenson * bookcase.h Removed the File->Close() action item. 2001-11-05 Robby Stephenson * bcgroupview.cpp Fixed the bug where clicking the collapse or expand menu item on a BCUnitItem caused the pixmap to change to a folder. 2001-11-04 Robby Stephenson * Released Bookcase version 0.1 diff --git a/RELEASE b/RELEASE new file mode 100644 index 00000000..22618dba --- /dev/null +++ b/RELEASE @@ -0,0 +1,13 @@ +Checklist for a Tellico release: +-------------------------------- +* Add entry in Changelog +* Set release version in top-level CMakeLists.txt +* Add release information to org.kde.tellico.appdata.xml +* Use releaseme/tarme.rb to create a tarball and test the build +* Tag the release, using vX.Y.Z pattern, and annotating the tag +* Update tarball and .md5 file +* Add new version to KDE bugzilla + https://bugs.kde.org/editproducts.cgi?action=edit&product=tellico + +--------------------------------- +https://community.kde.org/ReleasingSoftware diff --git a/org.kde.tellico.appdata.xml b/org.kde.tellico.appdata.xml index 71cfd100..084e2c1c 100644 --- a/org.kde.tellico.appdata.xml +++ b/org.kde.tellico.appdata.xml @@ -1,550 +1,555 @@ org.kde.tellico.desktop CC0-1.0 GPL-2.0+ Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico Tellico xxTellicoxx Tellico Collection Manager Menadžer kolekcija Gestor de col·leccions Gestor de col·leccions Správce sbírek Samlingshåndtering Sammlungsverwaltung Collection Manager Gestor de colecciones Kogude haldur Kokoelmahallinta Gestionnaire de collections Xestor de coleccións Gyűjteménykezelő Gerente de collection Pengelola Koleksi Gestore della collezione 소장품 관리 도구 Collectiebeheer Samlingsdatabase Zarządzanie zbiorem Gestor de Colecções Gerenciador de Coleções Менеджер коллекций Správca zbierok Upravljalnik zbirk Samlingshanterare Koleksiyon Yöneticisi Програма для керування збірками xxCollection Managerxx 收藏管理器

Tellico is an application for organizing your collections. It provides default templates for books, bibliographies, videos, music, video games, coins, stamps, trading cards, comic books, and wines. It allows you to enter your collection in a catalogue database, saving many different properties like title, author, etc.

Tellico je aplikacija za organiziranje vaše kolekcije. Omogućava početne predloške za knjige, bibliografije, videa, muziku, video igrice, kovanice, markice, kartice za razmjenu, stripove i vina. Dopušta vam ulaz u vašu kolekiju u katalošku bazu podataka, snimajući mnogo različitih osobina kao što su naslov, autor, itd.

El Tellico és una aplicació per organitzar col·leccions. Proporciona plantilles per defecte per a llibres, bibliografies, vídeos, música, videojocs, monedes, segells, cromos, còmics i vins. Permet introduir les col·leccions en una base de dades de catàlegs, desant moltes propietats diferents com el títol, l'autor, etc.

El Tellico és una aplicació per organitzar col·leccions. Proporciona plantilles per defecte per a llibres, bibliografies, vídeos, música, videojocs, monedes, segells, cromos, còmics i vins. Permet introduir les col·leccions en una base de dades de catàlegs, desant moltes propietats diferents com el títol, l'autor, etc.

Tellico er et program til at organisere dine samlinger. Det giver standardskabeloner til bøger, bibliografier, video, musik, computerspil, mønter, frimærker, byttekort, tegneserier og vine. Det lader dig angive din samling i en katalogdatabase, som gemmer mange forskellige egenskaber, såsom titel, forfatter osv.

Tellico ist eine Anwendung zur Organisation Ihrer Daten. Es enthält Standardvorlagen für Bücher, Literaturverzeichnisse, Videos, Musik, Videospiele, Comicbücher, Münzen, Briefmarken, Karten, Wein, Brettspiele und Dateikataloge. Sie können Ihre Sammlung in einer Katalog-Datenbank eingeben und viele verschiedene Eigenschaften wie Titel, Autor usw. darin speichern.

Tellico is an application for organising your collections. It provides default templates for books, bibliographies, videos, music, video games, coins, stamps, trading cards, comic books, and wines. It allows you to enter your collection in a catalogue database, saving many different properties like title, author, etc.

Tellico es una aplicación para organizar colecciones. Proporciona plantillas predeterminadas para libros, bibliografías, vídeos, música, videojuegos, monedas, sellos, cromos, cómics, y vinos. También le permite almacenar sus colecciones en una base de datos de catálogo, guardando diversas propiedades como el título, el autor, etc.

Tellico on kõikvõimalike kogude haldamiseks mõeldud rakendus. See pakub vaikimisi malle raamatute, bibliograafiate, videode, muusika, videomängude, müntide, markide, mängukaartide, koomiksite ja veinide tarbeks. Rakenduses saab oma kogu sisestada kataloogi andmebaasi, kuhu salvestatakse hulk omadusi, näiteks nimi, autor jne.

Tellico on sovellus kokoelmiesi järjestämiseen. Se tarjoaa oletusmallit kirjoille, lähdeluetteloille, videoille, musiikille, videopeleille, kolikoille, postimerkeille, keräilykorteille, sarjakuville ja viineille. Sen avulla voit syöttää kokoelmasi luettelotietokantaan eri ominaisuudet kuten otsikon, tekijän jne. säilyttäen.

Tellico est une application pour organiser vos collections. Il fournit des modèles par défaut pour les livres, les bibliographies, les vidéos, la musique, les jeux vidéo, les monnaies, les timbres, les cartes à collectionner, les bandes dessinées et les vins. Il vous permet de saisir votre collection dans une base de données de catalogue, enregistrant différentes propriétés telles que le titre, l'auteur, etc.

Tellico é un aplicativo para organizar coleccións. Fornece modelos predeterminados para libros, bibliografías, vídeos, música, videoxogos, moedas, selos, cartas de colección, banda deseñada, e vinos. Permítelle inserir a súa colección a unha base de datos de catálogo na que pode gardar moitas propiedades distintas, como o título de cada elemento, o seu autor, etc.

Tellico es un application pro organisar tu collectiones. Il forni patronos predefinite pro libros, bibliographias, videos, musica, video jocos, numisma, timbros, cartas de commercio, libros in designos e vinos. Il permitte te insertar tu collection inn un base de datos de catalogo, salveguardante multe proprietates como titulo, autor, etc.

Tellico adalah aplikasi untuk mengatur koleksi Anda. Ini menyediakan template baku untuk buku, bibliografi, video, musik, video game, koin, perangko, kartu perdagangan, buku komik, dan wine. Ini memungkinkan Anda untuk memasukkan koleksi Anda di database katalog, menyimpan banyak properti berbeda seperti judul, penulis, dll.

Tellico è un'applicazione per organizzare le tue collezioni. Fornisce modelli predefiniti per libri, bibliografie, video, musica, videogiochi, monete, francobolli, carte da gioco, fumetti e vini. Ti permette di inserire la tua collezione in una base di dati, salvando molte proprietà differenti come titolo, autore, ecc.

Tellico는 내 소장품을 관리하는 응용 프로그램입니다. 책, 서지, 비디오, 음악, 비디오 게임, 동전, 우표, 트레이드 카드, 만화 및 와인에 대한 기본 템플릿을 제공합니다. 카탈로그 데이터베이스에 소장품을 입력하여 제목, 작성자 등과 같은 다양한 속성을 저장할 수 있습니다.

Tellico is een toepassing voor het organiseren van uw verzamelingen. Het biedt standaard sjablonen voor boeken, bibliografieën, video's, muziek, videogames, munten, postzegels, visitekaartjes, strips en wijnen. Het stelt u in staat om uw verzameling in een catalogusdatabase in te voeren, waarin veel verschillende eigenschappen, zoals titel, auteur, etc. opgeslagen kunnen worden.

Tellico er eit program for å halda oversikt over samlingane dine. Det kjem med malar for bøker, litteraturlister, filmar, musikk, spel, myntar, frimerke, samlekort, teikneseriar og vin. Du kan katalogisera samlinga di i ein database og lagra mykje ulik informasjon, for eksempel tittel og forfattar.

Tellico jest programem do porządkowania twoich zbiorów. Dostarcza domyślnych szablonów dla książek, bibliografii, filmów, muzyki, gier wideo, monet, znaczków, kart handlowych, komiksów i win. Umożliwia wprowadzenie całego twojego zbioru do bazy danych katalogu, zapisując przy tym wiele różnych właściwości takich jak tytuł, autor, itp.

O Tellico é uma aplicação para organizar as suas colecções. Oferece modelos predefinidos para livros, bibliografias, vídeos, músicas, jogos de vídeo, moedas, selos, cromos, livros de banda desenhada, vinhos, entre outras colecções. Permite-lhe introduzir a sua colecção numa base de dados de catálogos, gravando diversas propriedades, como o título, o autor, etc.

Tellico é um aplicativo para organizar suas coleções. Oferece modelos predefinidos para livros, bibliografias, vídeos, músicas, jogos de vídeo game, moedas, selos, figurinhas, histórias em quadrinhos, vinhos, entre outras coleções. Permite-lhe introduzir sua coleção em um banco de dados de catálogos, gravando diversas propriedades, como título, autor, etc.

Tellico je aplikácia na organizáciu vašich zbierok. Poskytuje predvolené šablóny pre knihy, bibliografie, videá, hudbu, videohry, mince, známky, zberateľské karty, komiksy a vína. Umožní vám zadať vašu kolekciu do databázy katalógu, uložiť mnoho rozličných vlastností ako titul, autor atď.

Tellico je program za upravljanje vaših zbirk. Ponuja privzete predloge za knjige, bibliografije, videe, glasbo, igre, kovance, znamke, karte, stripe in vino. Omogoča vam, da vašo zbirko katalogizirate in shranite z različnimi lastnostmi kot so naslov, avtor, itd.

Tellico är ett program för att organisera samlingar. Det tillhandahåller standardmallar för böcker, bibliografier, videor, musik, videospel, mynt, frimärken, samlarbilder, serier och viner. Det låter dig mata in samlingen i en katalogdatabas, och spara många olika egenskaper som titel, författare, etc.

Tellico koleksiyonlarınızı yönetmek için bir programdır. Kitaplar, kaynakçalar, videolar, müzikler, video oyunları, paralar, pullar, kartlar, çizgi romanlar ve şaraplar için öntanımlı şablonlar içerir. Koleksiyonunuzu bir katalog veritabanına girmenize ve başlık, yazar gibi pek çok özelliğe göre kaydetmenize imkan tanır.

Tellico — програма для упорядковування ваших збірок. У ній передбачено типові шаблони для збірок книг, бібліографій, відео, музичних творів, відеоігор, нумізматики, марок, карток, коміксів та вин. За її допомогою ви зможете вести записи у форматі бази даних, зберігаючи різноманітні дані щодо елементів збірки, зокрема дані щодо назви, автора тощо.

xxTellico is an application for organizing your collections. It provides default templates for books, bibliographies, videos, music, video games, coins, stamps, trading cards, comic books, and wines. It allows you to enter your collection in a catalogue database, saving many different properties like title, author, etc.xx

Features:

Svojstva:

Característiques:

Característiques:

Vlastnosti:

Funktioner:

Funktionen:

Features:

Características:

Omadused:

Ominaisuuksia:

Fonctionnalités !

Funcionalidades:

Szolgáltatások:

Characteristicas:

Fitur:

Funzionalità:

기능:

Mogelijkheden:

Funksjonar:

Cechy:

Funcionalidades:

Funcionalidades:

Возможности:

Funkcie:

Zmožnosti:

Funktioner:

Özellikleri:

Можливості:

xxFeatures:xx

功能:

  • Supports default collections of books, bibliographic entries, videos, music, video games, comic books, coins, stamps, trading cards, wines, board games, and file catalogs
  • Podržava početne kolekcije knjiga, bibliografskih ulaza, videa, muzike, video igrica, stripova, kovanica, markica, kartica za razmjenu, vina, društvenih igara i datotečnih kataloga.
  • Funciona amb col·leccions per defecte de catàlegs de llibres, entrades bibliografies, vídeos, música, videojocs, còmics, monedes, segells, cromos, vins, jocs de taula i fitxers
  • Funciona amb col·leccions per defecte de catàlegs de llibres, entrades bibliografies, vídeos, música, videojocs, còmics, monedes, segells, cromos, vins, jocs de taula i fitxers
  • Understøtter standardsamlinger til bøger, bibliografier, video, musik, computerspil, mønter, frimærker, byttekort, tegneserier, vine, brætspil og filkataloger
  • Unterstützt Standardsammlungen für Bücher, Literaturverzeichnisse, Videos, Musik, Videospiele, Comicbücher, Münzen, Briefmarken, Karten, Wein, Brettspiele und Dateikataloge
  • Supports default collections of books, bibliographic entries, videos, music, video games, comic books, coins, stamps, trading cards, wines, board games, and file catalogues
  • Soporte para colecciones predeterminadas de libros, entradas bibliográficas, vídeos, música, videojuegos, cómics, monedas, sellos, cromos, vinos, juegos de mesa y catálogos de archivos
  • Vaikimisi on toetatud, raamatute, bibliograafiliste kirjete, videode, muusika, videomängude, koomiksite, müntide, markide, mängukaartide, veinide, lauamängude ja failikataloogide kogud
  • Tukee kirjojen, lähdeluettelotietueiden, videoiden, musiikin, videopelien, sarjakuvien, kolikoiden, postimerkkien, keräilykuvien, viinien, lautapelien ja tiedostoluettelojen oletuskokoelmia
  • Prends en charge par défaut des collections de livres, d'entrées bibliographiques, de vidéos, de musique, de jeux vidéo, de bandes dessinées, de monnaies, de timbres,de cartes à collectionner, de vins, de jeux de plateau et de catalogues de fichiers.
  • Compatíbel con coleccións predeterminadas de libros, entradas bibliográficas, vídeos, música, videoxogos, banda deseñada, moedas, selos, cartas de colección, viños, xogos de taboleiro e catálogos de ficheiros.
  • Il supporta collectiones predefinite de libros, entratas bibliographic, videos, musica, video jocos, libros in designo, numisma, timbros, cartas de commerciar, vinos, jocos de tabula e catalogos de files
  • Mendukung koleksi buku, entri bibliografi, video, musik, video game, buku komik, koin, perangko, kartu perdagangan, wine, permainan papan, dan katalog file standar yang didukung
  • Supporta collezioni standard di libri, voci bibliografiche, video, musica, videogiochi, fumetti, monete, francobolli, carte da gioco, vini, giochi da tavolo e collezioni di file
  • 서적, 서지 정보 항목, 비디오, 음악, 비디오 게임, 만화책, 동전, 우표, 트레이딩 카드, 와인, 보드 게임 및 파일 카탈로그의 기본 소장품 지원
  • Ondersteunt standaard verzamelingen van boeken, bibliografie-items, video's, muziek, videogames, strips, munten, postzegels, visitekaartjes, wijnen, bordspellen en catalogs van bestanden
  • Støttar samlingar av bøker, litteraturlister, filmar, musikk, spel, teikneseriar, myntar, frimerke, samlekort, vin, brettspel og filkatalogar
  • Obsługa domyślnych zbiorów książek, bibliografii, filmów, muzyki, gier wideo, monet, znaczków, kart handlowych, komiksów i win, gier planszowych oraz katalogów plików
  • Suporta diversas colecções de livros, itens bibliográficos, vídeos, músicas, jogos de vídeo, livros de banda desenhada, moedas, selos, cromos, vinhos, jogos de tabuleiros e catálogos de ficheiros
  • Suporte a diversas coleções de livros, itens bibliográficos, vídeos, músicas, jogos de vídeo game, histórias em quadrinhos, moedas, selos, figurinhas, vinhos, jogos de tabuleiro e catálogos de arquivos
  • Podporuje predvolené zbierky kníh, bibliografických položiek, videí, hudby, videohier, komiksov, mincí, známok, zberateľských kariet, vín, stolných hier a zakladačov
  • Podpira privzete zbirke za knjige, bibliografije, videe, glasbo, igre, kovance, znamke, karte, stripe, namizne igre, vino in zbirke datotek
  • Stöder standardsamlingar för böcker, bibliografiposter, videor, musik, videospel, serier, mynt, frimärken, samlarbilder, viner, brädspel och filkataloger
  • Kitapların, kaynakça girdilerinin, videoların, müziklerin, video oyunlarının, çizgi romanların, paraların, pulların, kartların, şarapların, oyunların ve dosya katalogların öntanımlı koleksiyonlarını destekler
  • Підтримка типових збірок книг, бібліографічних записів, відео, музики, відеоігор, коміксів, нумізматики, марок, карток, вин, настільних ігор та каталогів файлів.
  • xxSupports default collections of books, bibliographic entries, videos, music, video games, comic books, coins, stamps, trading cards, wines, board games, and file catalogsxx
  • Supports user-defined custom collections
  • Podržava korisnički definisane prilagođene kolekcije
  • Funciona amb col·leccions personalitzades definides per l'usuari
  • Funciona amb col·leccions personalitzades definides per l'usuari
  • Understøtter brugerdefinerede tilpassede samlinger
  • Unterstützt benutzerdefinierte Sammlungen
  • Supports user-defined custom collections
  • Soporta colecciones personalizadas definidas por el usuario
  • Toetatud on ka kasutaja loodud kohandatud kogud
  • Tukee käyttäjän mukauttamia kokoelmia
  • Prends en charge les collections personnalisées par l'utilisateur
  • Permite aos usuarios definir coleccións personalizadas.
  • Il supporta collectiones personalisate definite per le usator
  • Dukungan koleksi kustom yang ditentukan pengguna
  • Supporta collezioni personalizzate definite dall'utente
  • 사용자 정의 맞춤 소장품 지원
  • Ondersteunt gebruikergedefinieerde eigen verzamelingen
  • Støttar eigendefinerte samlingstypar
  • Obsługa zbiorów określonych przez użytkownika
  • Suporta colecções personalizadas pelo utilizador
  • Suporte a coleções personalizadas pelo usuário
  • Podporuje používateľom definované vlastné zbierky
  • Podpira uporabniško določene zbirke
  • Stöder användardefinierade anpassade samlingar
  • Kullanıcı tanımlı özel koleksiyonları destekler
  • Підтримка визначених користувачем нетипових збірок.
  • xxSupports user-defined custom collectionsxx
  • Supports any number of user-defined fields, of several different types: text, paragraph, list, checkbox, number, URL, date, images, and combinations
  • Podržava bilo koji broj korisnički definisanih polja, nekoliko različitih tipova: tekst, paragraf, lista, kvadtratić za označavanje, broj, URL, datum, slike i kombinacije.
  • Funciona amb qualsevol nombre de camps definits per l'usuari, de molts tipus diferents: text, paràgraf, llista, casella de selecció, nombre, URL, data, imatges i combinacions
  • Funciona amb qualsevol nombre de camps definits per l'usuari, de molts tipus diferents: text, paràgraf, llista, casella de selecció, nombre, URL, data, imatges i combinacions
  • Understøtter brugerdefinerede felter af mange forskellige typer, tekst, afsnit, liste, afkrydsningsfelt, nummer, URL, dato, billeder og kombinationer
  • Unterstützt eine beliebige Anzahl von benutzerdefinierten Feldern mit verschiedenen Typen: Text, Absatz, Liste, Ankreuzfeld, Nummer, Adresse (URL), Datum, Bilder und Kombinationen
  • Supports any number of user-defined fields, of several different types: text, paragraph, list, tickbox, number, URL, date, images, and combinations
  • Soporta cualquier número de campos definidos por el usuario, de varios tipos diferentes: texto, párrafo, lista, checkbox, número, URL, fecha, imágenes y combinaciones.
  • Toetatud on mis tahes hulk kasutaja määratud välju, mis võivad olla väga eri tüüpi: tekst, lõik, loend, märkekast, arv, URL, kuupäev, pilt või nende kombinatsioon
  • Tukee rajattomasti käyttäjän määrittämiä useamman tyypin kenttiä (teksti, kappale, luettelo, valintaruutu, luku-, verkko-osoite, päiväys, kuvat ja niiden yhdistelmät)
  • Prend en charge n'importe quel nombre de champs définis par l'utilisateur, de plusieurs types différents : texte, paragraphe, liste, case à cocher, nombre, URL, date, images et combinaisons
  • Permite ao usuario definir campos de varios tipos: texto, parágrafo, lista, lista con caixas para marcar, número, URL, data, imaxes, e combinacións.
  • Il supporta qualcunque numero de campos definite per le usator, de plure typos differente: texto, paragrapho, lista, quadrato de selection, numero, URL, data, imagines e combinationes
  • Mendukung angka apa pun dari bidang yang ditentukan, beberapa tipe yang berbeda: teks, paragraf, daftar, kotak centang, nomor, URL, tanggal, gambar, dan kombinasi
  • Supporta un qualsiasi numero di campi definiti dall'utente, di molti tipi differenti: testo, paragrafi, liste, caselle, numeri, URL, dati, immagini e combinazioni
  • 텍스트, 단락, 목록, 체크 상자, 번호, URL, 날짜, 이미지 및 조합 등 다양한 유형의 사용자 정의 필드 지원
  • Ondersteunt elk aantal gebruikergedefinieerde velden met verscheidene verschillende types: tekst, paragraaf, lijst, keuzevakje, aantal, URL, datum, afbeelding en combinaties
  • Støttar vilkårleg mange eigendefinerte felt i ulike variantar: tekst, avsnitt, liste, avkryssingsboks, tal, nettadresse, dato, bilete og kombinasjonar av desse variantane
  • Obsługa dowolnej liczby pól określonych przez użytkownika, wielu różnych rodzajów: tekst, akapit, spis, pole zaznaczane, liczba, adres URL, data, obraz i ich kombinacje
  • Suporta qualquer número de campos definidos pelo utilizador, de diferentes tipos: texto, parágrafo, lista, opção de marcação, número, URL, data, imagens e combinações
  • Suporte a qualquer número de campos definidos pelo usuário, de diferentes tipos: texto, parágrafo, lista, opção de marcação, número, URL, data, imagens e combinações
  • Podporuje ľubovoľný počet používateľom definovaných polí rôznych typov: text, odstavec, zoznam, checkbox, číslo, URL, dátum, obrázky a kombinácie
  • Podpira poljubno število uporabniško določenih polj različnih vrst: besedilo, odstavek, seznam, označno polje, številka, naslov URL, datum, slike in njihove poljubne kombinacije
  • Stöder hur många användardefinierade fält som helst av flera olika typer: text, stycke, lista, kryssruta, tal, webbadress, datum, bilder och kombinationer
  • Çok farklı türlerde kullanıcı tanımlı alanları destekler: metin, paragraf, liste, işaret kutusu, sayı, URL, tarih, resim ve bunların bileşimleri
  • Підтримка довільної кількості визначених користувачем полів записів: текст, абзац, список, поле для позначки, номер, адреса, дата, зображення та будь-які інші комбінації.
  • xxSupports any number of user-defined fields, of several different types: text, paragraph, list, checkbox, number, URL, date, images, and combinationsxx
  • Handles entries with multiple authors, genres, keywords, etc
  • Upravlja ulazima sa više autora, žanrova, ključnih riječi, itd.
  • Gestiona entrades amb diversos autors, gèneres, paraules clau, etc.
  • Gestiona entrades amb diversos autors, gèneres, paraules clau, etc.
  • Kan håndtere indgange med flere forfattere, genrer, nøgleord osv.
  • Verarbeitet Einträge mit mehreren Autoren, Genres, Stichwörtern usw.
  • Handles entries with multiple authors, genres, keywords, etc
  • Maneja entradas con varios autores, géneros, palabras clave, etc
  • Raskusi ei valmista ka kirjed, mis sisaldavad mitut autorit, žanri, võtmesõna jne.
  • Käsittelee tietueet, joissa on useampia tekijöitä, tyylilajeja, avainsanoja jne.
  • Gestion des entrées avec plusieurs auteurs, genres, mots-clés, etc
  • Pode xestionar entradas con varios autores, xéneros, palabras clave, etc.
  • Il manea entratas con multiple autores, generes, parolas clave, etc.
  • Menangani entri-entri dengan beberapa penulis, genre, kata kunci, dll.
  • Gestisce voci con autori multipli, generi, parole chiave, ecc.
  • 여러 저자, 장르, 키워드 등으로 항목 처리
  • Behandelt items met meerdere auteurs, genres, zoektermen, etc.
  • Kan handtera oppføringar som inneheld fleire forfattarar, sjangrar, nøkkelord, …
  • Obsługa wpisów z wieloma autorami, gatunkami, słowami kluczowymi, itp.
  • Lida com itens de vários autores, géneros, palavras-chave, etc
  • Lida com itens de vários autores, gêneros, palavras-chave, etc
  • Spracúva položky s viacerými autormi, žánrami, kľúčovými slovami atď.
  • Zna ravnati z vnosi z večimi avtorji, zvrstmi, ključnimi besedami, itd.
  • Hanterar poster med flera författare, genrer, nyckelord, etc.
  • Birden çok yazarı, türü ve anahtar kelimeyi işleyebilir
  • Обробка записів з декількома авторами, жанрами, ключовими словами тощо.
  • xxHandles entries with multiple authors, genres, keywords, etcxx
  • Automatically formats titles and names
  • Automatski formatira naslove i imena
  • Formata automàticament els títols i els noms
  • Formata automàticament els títols i els noms
  • Formaterer titler og navne automatisk
  • Automatische Formatierung von Titeln und Namen
  • Automatically formats titles and names
  • Formatea automáticamente títulos y nombres
  • Pealkirjasid ja nimesid saab lasta automaatselt vormindada
  • Muotoilee otsikot ja nimet automaattisesti
  • Formate automatiquement les titres et les noms
  • Aplica automaticamente un formato aos títulos e nomes.
  • Automaticamente da forma a titulos e nomines
  • Format secara otomatis judul-judul dan nama-nama
  • Formatta automaticamente titoli e nomi
  • 제목과 이름의 자동 서식 지정
  • Deelt automatisch titels en namen in
  • Automatisk formatering av titlar og namn
  • Samoczynne formatowanie tytułów i nazw
  • Formata automaticamente os títulos e nomes
  • Formata automaticamente os títulos e nomes
  • Automaticky formátuje tituly a názvy
  • Samodejno oblikuje naslove in imena
  • Formaterar automatiskt titlar och namn
  • Başlık ve isimleri otomatik biçimlendirir
  • Автоматичне форматування заголовків та назв.
  • xxAutomatically formats titles and namesxx
  • Supports collection searching and view filtering
  • Podržava pretragu kolekcije i filter pregleda
  • Admet la cerca de col·leccions i filtratge de la vista
  • Admet la cerca de col·leccions i filtratge de la vista
  • Understøtter søgning i samlinger og filtrering af visning
  • Unterstützt das Durchsuchen von Sammlungen und Filterung der Ansichten
  • Supports collection searching and view filtering
  • Soporta búsqueda de colecciones y filtrado de la vista
  • Toetatud on kogudes otsimine ja vaate filtreerimine
  • Tukee kokoelmien hakua ja näkymien suodatusta
  • Prend en charge la recherche dans la collection et une vue filtrée
  • Permite buscar nas coleccións e filtrar nas vistas.
  • Il supporta cerca de collection e filtro de vista
  • Dukungan pencarian koleksi dan pemfilteran tampilan
  • Supporta ricerche nella collezione e filtraggio della vista
  • 소장품 검색 및 보기 필터링 지원
  • Ondersteunt zoeken in verzamelingen en filtering op weergave
  • Støttar søk og filtrering i samlingar
  • Obsługa przedukiwania zbiorów i filtrowanych widoków
  • Suporta a pesquisa na colecção e a filtragem da janela
  • Suporte a pesquisa na coleção e filtragem da janela
  • Podporuje hľadanie kolekcií a filtrovanie zobrazenia
  • Podpira iskanje po zbirkah in filtriranje prikaza
  • Stöder sökning i samlingen och filtrering av visningen
  • Koleksiyon araması ve filtreleyerek görüntülemeyi destekler
  • Підтримка пошуку у збірці та фільтрування даних для перегляду.
  • xxSupports collection searching and view filteringxx
  • Sorts and groups collection by various properties
  • Sortira i grupira kolekcije po različitim osobinama
  • Ordena i agrupa les col·leccions per diverses propietats
  • Ordena i agrupa les col·leccions per diverses propietats
  • Sorterer og grupperer samling efter diverse egenskaber
  • Sortiert und Gruppiert Sammlungen nach verschiedenen Eigenschaften
  • Sorts and groups collection by various properties
  • Ordena y agrupa las colecciones en función de diversas propiedades
  • Kogusid saab sortida ja rühmitada eri omaduste järgi
  • Lajittelee ja ryhmittelee kokoelmia eri ominaisuuksien mukaan
  • Trie et groupe les collections suivant diverses propriétés
  • Permite ordenar ou agrupar as coleccións en funcións de varias propiedades.
  • Il ordina e gruppa per varie proprietates
  • Mengurutkan dan mengelompokkan koleksi dengan berbagai properti
  • Ordina e raggruppa la collezione in base a varie proprietà
  • 다양한 속성별 소장품 정렬 및 그룹
  • Sorteert en groepeert verzameling door verschillende eigenschappen
  • Sorterer og grupperer automatisk samlingane
  • Szeregowanie i grupowanie zbiorów według różnych właściwości
  • Ordena e agrupa a colecção de acordo com diversas propriedades
  • Ordena e agrupa a coleção de acordo com diversas propriedades
  • Triedi a zoskupuje kolekciu podľa rôznych vlastností
  • Zna razvrstiti in združevati zbirko po različnih lastnostih
  • Sorterar och grupperar samlingen enligt olika egenskaper
  • Koleksiyonları çeşitli özelliklerine göre sıralar ve gruplar
  • Упорядковування та групування збірки за різноманітними властивостями.
  • xxSorts and groups collection by various propertiesxx
  • Allows customizable entry templates through XSLT
  • Dopušta prilagođene ulazne predloške preko XSLT
  • Admet plantilles d'entrades personalitzades mitjançant XSLT
  • Admet plantilles d'entrades personalitzades mitjançant XSLT
  • Muliggør indgangsskabeloner som kan tilpasses via XSLT
  • Erlaubt anpassbare Vorlagen für Einträge mittels XSLT
  • Allows customisable entry templates through XSLT
  • Permite plantillas personalizadas de entradas a través de XSLT
  • XSLT abil on võimalik luua kohandatud kirjemalle
  • Sallii mukautetut tietuemallit XSLT:n avulla
  • Permet des modèles d'entrées personnalisable via XSLT
  • Permite personalizar os modelos para entradas mediante XSLT.
  • Il permitte patronos de entratas personalisate per XSLT
  • Kemungkinan men-template-kan entri yang dapat dikustomisasi melalui XSLT
  • Permette modelli di immissione personalizzabili attraverso XSLT
  • XSLT를 통해 사용자 지정 가능한 항목 템플릿 허용
  • Staat aanpasbare sjablonen van items toe via XSLT
  • Kan tilpassa oppføringsmalar ved hjelp av XSLT-stilsett
  • Własne szablony wpisów dzięki XSLT
  • Permite modelos de registos personalizados, através de XSLT
  • Permite modelos de registros personalizados, através de XSLT
  • Umožňuje upraviteľné šablóny položiek cez XSLT
  • Dovoljuje uporabno prilagodljivih predlog za vnos preko XSLT
  • Tillåter anpassningsbara inmatningsmallar via XSLT
  • XSLT üzerinden girdi şablonlarının özelleştirilmesine izin verir
  • Придатні до налаштовування шаблони записів на основі XSLT.
  • xxAllows customizable entry templates through XSLTxx
  • Imports MODS, Bibtex, RIS, CSV, PDF metadata, and many other formats
  • Uvozi iz MODS, Bibtex, RIS, CSV, PDF metapodatke, i mnogih drugih formata
  • Importa MODS, Bibtex, RIS, CSV, metadades de PDF, i molts altres formats
  • Importa MODS, Bibtex, RIS, CSV, metadades de PDF, i molts altres formats
  • Importerer MODS, Bibtex, RIS, CSV, PDF-metadata og mange andre formater
  • Import von MODS-, Bibtex-, RIS-, CSV-, PDF-Metadaten und vielen anderen Formaten
  • Imports MODS, Bibtex, RIS, CSV, PDF metadata, and many other formats
  • Importa MODS, Bibtex, RIS, CSV, metadatos PDF y muchos otros formatos
  • MODS-i, Bibtexi, RIS-i, CSV, PDF-i metaandmete ja veel paljude vormingute import
  • Tuo MODS-, BibTeX-, RIS-, CSV-, PDF-metatieto- ja muista tiedostomuodoista
  • Importe MODS, Bibtex, RIS, CSV, les métadonnées PDF, et beaucoup d'autres formats
  • Pode importar ficheiros en formato MODS, Bibtex, RIS, CSV, metadatos de PDF, e moitos outros formatos.
  • Il importa metadatos MODS, Bibtex, RIS, CSV, PDF e plure altere formatos
  • Mengimporkan format MODS, Bibtex, RIS, CSV, PDF metadata, dan banyak format lain
  • Importa MODS, Bibtex, RIS, CSV, metadati dei PDF, e molti altri formati
  • MODS, Bibtex, RIS, CSV, PDF 메타데이터 및 기타 다양한 형식 가져오기
  • Importeert MODS, Bibtex, RIS, CSV, PDF metadata en vele andere formaten
  • Kan importera metadata frå blant anna MODS-, BibTeX-, RIS-, CSV- og PDF-filer
  • Importowanie MODS, Bibtex, RIS, CSV, metadanych PDF i wielu innych formatów
  • Importa dos formatos MODS, Bibtex, RIS, CSV, meta-dados de PDF, entre muitos outros
  • Importa dos formatos MODS, Bibtex, RIS, CSV, metadados de PDF, entre muitos outros
  • Importuje MODS, Bibtex, RIS, CSV, PDF metadáta a veľa iných formátov
  • Zna uvoziti MODS, Bibtex, RIS, CSV, metapodatke PDF in druge vrste datotek
  • Importerar MODS, Bibtexd, RIS, CSV, PDF-metadata och många andra format
  • MODS, Bibtex, RIS, CSV, PDF üst verisi ve pek çok diğer biçimi içe aktarır
  • Імпортування метаданих MODS, Bibtex, RIS, CSV, PDF та даних у багатьох інших форматах.
  • xxImports MODS, Bibtex, RIS, CSV, PDF metadata, and many other formatsxx
  • Exports to Bibtex, ONIX, CSV, HTML, and other formats
  • Exporta a Bibtex, ONIX, CSV, HTML i altres formats
  • Exporta a Bibtex, ONIX, CSV, HTML i altres formats
  • Export zu Bibtex, ONIX, CSV, HTML, und andere Formate
  • Exports to Bibtex, ONIX, CSV, HTML, and other formats
  • Exporta a Bibtex, ONIX, CSV, HTML y otros formatos
  • Vie BibTeX-, ONIX-, CSV-, HTML- ja muihin tiedostomuotoihin
  • Exporte vers Bibtex, ONIX, CSV, HTML et beaucoup d'autres formats
  • Pode exportar coleccións nos formatos Bibtex, CSV, e HTML, entre outros.
  • Pengeksporan ke format Bibtex, ONIX, CSV, HTML, dan format lain
  • Esporta in formato Bibtex, ONIX, CSV, HTML e altri
  • Bibtex, ONIX, CSV, HTML 및 기타 형식으로 내보내기
  • Exporteert naar Bibtex, ONIX, CSV, HTML en andere formaten
  • Kan eksportera til BibTeX, ONIX, CSV, HTML og andre format
  • Eksportowanie Bibtex, ONIX, CSV, HTML i innych formatów
  • Exporta para o Bibtex, ONIX, CSV, HTML, entre outros formatos
  • Exporta para o Bibtex, ONIX, CSV, HTML, entre outros formatos
  • Exportuje do Bibtex, ONIX, CSV, HTML a iných formátov
  • Zna izvoziti v Bibtex, ONIX, CSV, HTML in druge vrste datotek
  • Exporterar till Bibtex, ONIX, CSV, HTML och andra format
  • Bibtex, ONIX, CSV, HTML, PilotDB ve pek çok diğer biçimi dışa aktarır
  • Експортування даних у форматах Bibtex, ONIX, CSV, HTML тощо.
  • xxExports to Bibtex, ONIX, CSV, HTML, and other formatsxx
  • Imports information directly from Amazon.com, IMDb, z39.50 servers, PubMed, SRU servers, CrossRef.org, various other websites, and from external scripts
  • Uvozi informacije direktno sa Amazon.com, IMDb, z39.50 servera, PubMed, SRU servera, CrossRef.org, različitih drugih web stranica, i iz vanjskih skripti
  • Importa informació directament des d'Amazon.com, IMDb, servidors z39.50, PubMed, servidors SRU, CrossRef.org, altres llocs web, i des de scripts externs
  • Importa informació directament des d'Amazon.com, IMDb, servidors z39.50, PubMed, servidors SRU, CrossRef.org, altres llocs web, i des de scripts externs
  • Importerer information direkte fra Amazon.com, IMDb, z39.50 servers, PubMed, SRU-servere, CrossRef.org, diverse andre websider og fra eksterne scripts
  • Importiert Informationen direkt von Amazon.com, IMDb, z39.50-Servern, PubMed, SRU-Servern, CrossRef.org, verschiedenen anderen Webseiten und von externen Skripten
  • Imports information directly from Amazon.com, IMDb, z39.50 servers, PubMed, SRU servers, CrossRef.org, various other websites, and from external scripts
  • Importa información directamente de Amazon.com, IMDb, z39.50 servers, PubMed, SRU servers, CrossRef.org, y más sitios web, además de hacerlo desde scripts externos
  • Teabe importimine otse Amazon.com-ist, IMDb-st, z39.50 serveritest, PubMed-ist, SRU serveritest, CrossRef.org-ist, veel paljudelt veebilehekülgedelt ja väliste skriptide kaudu
  • Tuo tietoa suoraan Amazon.comista, IMDb:stä, z39.50-palvelimilta, PubMedistä, SRU-palvelimilta, CrossRef.orgista, muilta verkkosivuilta ja ulkoisista skripteistä
  • Importe les informations directement depuis Amazon.com, IMDb, z39.50, PubMed, SRU, CrossRef.org et de nombreux autres sites web ainsi que depuis des scripts externes
  • Importa información directamente de Amazon.com, IMDb, servidores z39.50, PubMed, servidores SRU, CrossRef.org, doutros sitios web, e de scripts externos.
  • Il importa information directemente ab Amazon.com, IMDb, servitores z39.50, PubMed, seritores SRU, CrossRef.org, varie altere sitios web e ab external scripts
  • Pengimporkan informasi secara langsung dari situs Amazon.com, IMDb, z39.50 servers, PubMed, SRU servers, CrossRef.org, berbagai situs lain, dan dari skrip eksternal
  • Importa informazioni direttamente da Amazon.com, IMDb, server z39.50, PubMed, server SRU, CrossRef.org, svariati altri siti web, e da script esterni
  • Amazon.com, IMDb, z39.50 서버, PubMed, SRU 서버, CrossRef.org, 기타 여러 웹 사이트 및 외부 스크립트에서 직접 정보 가져오기
  • Importeert informatie direct uit Amazon.com, IMDb, z39.50-servers, PubMed, SRU-servers, CrossRef.org, verschillende andere websites en uit externe scripts
  • Kan henta informasjon frå Amazon.com, IMDb, z39.50-tenarar, PubMed, SRU-tenarar, CrossRef.org, ymse andre nettstadar og eksterne skript
  • Importowanie szczegółów bezpośrednio z Amazon.com, IMDb, serwerów z39.50, PubMed, serwerów SRU, CrossRef.org i różnych innych stron sieciowych, a także z zewnętrznych skryptów
  • Importa a informação directamente a partir da Amazon.com, IMDb, servidores de z39.50, PubMed, servidores de SRU, CrossRef.org, entre outros serviços, bem como de programas externos
  • Importa a informação diretamente a partir da Amazon.com, IMDb, servidores z39.50, PubMed, servidores SRU, CrossRef.org, entre outros serviços, bem como de scripts externos
  • Importuje informácie priamo z Amazon.com, IMDb, z39.50 serverov, PubMed, SRU serverov, CrossRef.org, rôznych iných webstránok a z externých skriptov
  • Zna uvoziti podatke neposredno iz Amazon.com, IMDb, strežnikov z39.50, PubMed, strežnikov SRU, CrossRef.org, drugih spletnih strani in zunanjih skript
  • Importerar information direkt från Amazon.com, IMDb, z39.50-servrar, PubMed, naSRU-servrar, CrossRef.org, diverse andra webbplatser, och från externa skript
  • Bilgileri doğrudan Amazon.com, IMDb, z39.50 sunucuları, PubMed, SRU sunucuları, CrossRef.org ve çeşitli başka sitelerden ve harici betiklerden içe aktarır
  • Імпортування даних безпосередньо із серверів Amazon.com, IMDb, z39.50, PubMed, SRU, CrossRef.org, інших сайтів та на основі зовнішніх скриптів.
  • xxImports information directly from Amazon.com, IMDb, z39.50 servers, PubMed, SRU servers, CrossRef.org, various other websites, and from external scriptsxx
  • Imports CDDB data for cataloging audio CDs
  • Uvozi CDDB podatke za popisivanje muzičkih CD-ova
  • Importa dades CDDB per a la catalogació dels CD d'àudio
  • Importa dades CDDB per a la catalogació dels CD d'àudio
  • Importerer CDDB-data til katalogisering af lyd-cd'er
  • Importiert CDDB-Daten für die Katalogisierung von Audi-CDs
  • Imports CDDB data for cataloguing audio CDs
  • Importa datos CDDB para catalogar CD de audio
  • CDDB andmete import audio-CD-de kataloogimiseks
  • Tuo ääni-CD:iden luokitteluun tarvittavat CDDB-tiedot
  • Importe les données CDDB pour cataloguer les CDs audio
  • Importa datos de CDDB para catalogar CD de son.
  • Il importa datos de CDDB pro catalogar CDs audio
  • Pengimporan data CDDB untuk pengkatalogan CD-CD audio
  • Importa dati CDDB per catalogare cd audio
  • 오디오 CD 카탈로그 작성을 위한 CDDB 데이터 가져오기
  • Importeert CDDB-gegevens voor catalogiseren van audio-cd's
  • Kan importera CDDB-data for lyd-CD-ar
  • Importowanie danych CDDB do katalogowanie płyt muzycznych
  • Importa os dados do CDDB para catalogar os CD's de áudio
  • Importa os dados CDDB para catalogar CDs de áudio
  • Importuje údaje CDDB pre katalogizáciu CD
  • Zna uvoziti podatke CDDB za katalogiziranje glasbenih CD-jev
  • Importerar CDDB-data för katalogisering av ljud-cd
  • CD'leri kataloglamak için CDDB verisini içe aktarır
  • Імпортування даних CDDB для каталогізації звукових компакт-дисків.
  • xxImports CDDB data for cataloging audio CDsxx
  • Scans and imports audio file collections, such as mp3 or ogg
  • Pretražuje i uvozi kolekcije muzičkih datoteka kao što su mp3 ili ogg
  • Explora i importa col·leccions de fitxers d'àudio, com MP3 o OGG
  • Explora i importa col·leccions de fitxers d'àudio, com MP3 o OGG
  • Scanner og importerer lydfilsamlinger såsom mp3 eller ogg
  • Durchsucht und importiert Sammlungen von Audio-Dateien wie zum Beispiel mp3 oder ogg
  • Scans and imports audio file collections, such as mp3 or ogg
  • Escanea e importa colecciones de archivos de audio, como por ejemplo mp3 u ogg
  • Helifailide (näiteks mp3 või ogg) kogude läbiuurimine ja import
  • Etsii ja tuo äänitiedostojen kuten MP3:ten ja Oggien kokoelmia
  • Scanne et importe les collections de fichiers audio, tels que les fichiers mp3 ou ogg
  • Analiza e importa coleccións de ficheiros de son, como MP3 ou OGG.
  • Il scande e importa collectiones de file audio, tal como mp3 o ogg
  • Pemindaian dan pengimporan koleksi file audio, sepertihalnya mp3 atau ogg
  • Cerca e importa collezioni di file audio, come mp3 oppure ogg
  • MP3 또는 OGG 같은 오디오 파일 소장품 검색 및 가져오기
  • Scant en importeert verzamelingen van audio-bestanden, zoals mp3 of ogg
  • Søkjer etter og importerer lydfilsamlingar, som MP3 og OGG
  • Przeszukiwanie i importowanie zbiorów plików dźwiękowych, takich jak mp3 czy ogg
  • Sonda e importa as colecções dos ficheiros de áudio, como MP3's ou OGG's
  • Verifica e importa as coleções de arquivos de áudio, como MP3 ou OGG
  • Prehľadá a importuje zbierky audio súborov, ako mp3 alebo ogg
  • Zna preiskati in uvoziti zbirke zvočnih datotek kot so mp3 ali ogg
  • Söker igenom och importera samlingar av ljudfiler, såsom mp3 eller ogg
  • Mp3 ve ogg gibi ses dosyalarını tarar ve içe aktarır
  • Сканування та імпортування збірок звукових файлів, зокрема mp3 та ogg.
  • xxScans and imports audio file collections, such as mp3 or oggxx
http://tellico-project.org https://bugs.kde.org/enter_bug.cgi?format=guided&product=tellico http://tellico-project.org/faq https://docs.kde.org/development/en/extragear-office/tellico/index.html The main window showing a simple item in a book collection La finestra principal mostra un element senzill en una col·lecció de llibres La finestra principal mostra un element senzill en una col·lecció de llibres Das Hauptfenster mit einem einfachen Eintrag einer Bücher-Sammlung The main window showing a simple item in a book collection La ventana principal mostrando un elemento sencillo de una colección de libros La fenêtre principale, qui affiche un élément simple au sein d'une collection de livres A xanela principal, mostrando un elemento sinxelo nunha colección de libros Window utama menampilkan item sederhana dalam koleksi buku La finestra principale che mostra un semplice elemento in una collezione di libri 도서 소장품의 간단한 항목을 표시하는 주 창 Het hoofdvenster die een eenvoudig item in een boekenverzameling toont Hovudvindauget som viser ei enkel oppføring i ei boksamling Okno główne pokazujące prosty element w zbiorze książek A janela principal a mostrar um único item numa colecção de livros A janela principal a mostrar um único item em uma coleção de livros Huvudfönstret som visar ett enkelt objekt i en boksamling Головне вікно програми із простим записом у збірці книжок xxThe main window showing a simple item in a book collectionxx https://cdn.kde.org/screenshots/tellico/tellico.png The main window showing side-by-side views of a game collection La finestra principal mostra vistes un al costat de l'altra d'una col·lecció de jocs La finestra principal mostra vistes un al costat de l'altra d'una col·lecció de jocs The main window showing side-by-side views of a game collection La ventana principal mostrando vistas en paralelo de una colección de juegos + A xanela principal mostrando as vistas lado a lado dunha colección de xogos Window utama menampilkan tampilan sisi ke sisi dari sebuah koleksi permainan La finestra principale che mostra le viste affiancate di una collezione di libri 게임 소장품을 나란히 표시하는 주 창 Het hoofdvenster die een spellenverzameling naast elkaar toont + Okno główne pokazujące widoki zbioru gier obok siebie A janela principal a mostrar vistas lado-a-lado de uma colecção de jogos A janela principal mostrando visões lado a lado de uma coleção de jogos Huvudfönstret som visar en spelsamling med visning sida vid sida Головне вікно програми із панелями паралельного перегляду збірки ігор xxThe main window showing side-by-side views of a game collectionxx https://cdn.kde.org/screenshots/tellico/tellico-games.png KDE tellico https://tellico-project.org/tellico-31-released https://tellico-project.org/tellico-311-released https://tellico-project.org/tellico-312-released https://tellico-project.org/tellico-313-released https://tellico-project.org/tellico-314-released https://tellico-project.org/tellico-32-released + + https://tellico-project.org/tellico-321-released +
diff --git a/src/barcode/barcode_v4l.cpp b/src/barcode/barcode_v4l.cpp index cd808910..57c2d0c4 100644 --- a/src/barcode/barcode_v4l.cpp +++ b/src/barcode/barcode_v4l.cpp @@ -1,249 +1,249 @@ /*************************************************************************** Copyright (C) 2007-2009 Sebastian Held ***************************************************************************/ /*************************************************************************** * * * 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 . * * * ***************************************************************************/ // uses code from v4lgrab.c (c) Linux Kernel 2.6.30 #include "barcode_v4l.h" #include "../tellico_debug.h" #include /* low-level i/o */ #include #include extern "C" { #include } using barcodeRecognition::barcode_v4l; #define READ_VIDEO_PIXEL(buf, format, depth, r, g, b) \ { \ switch (format) \ { \ case VIDEO_PALETTE_GREY: \ switch (depth) \ { \ case 4: \ case 6: \ case 8: \ (r) = (g) = (b) = (*buf++ << 8);\ break; \ \ case 16: \ (r) = (g) = (b) = \ *((unsigned short *) buf); \ buf += 2; \ break; \ } \ break; \ \ \ case VIDEO_PALETTE_RGB565: \ { \ unsigned short tmp = *(unsigned short *)buf; \ (r) = tmp&0xF800; \ (g) = (tmp<<5)&0xFC00; \ (b) = (tmp<<11)&0xF800; \ buf += 2; \ } \ break; \ \ case VIDEO_PALETTE_RGB555: \ (r) = (buf[0]&0xF8)<<8; \ (g) = ((buf[0] << 5 | buf[1] >> 3)&0xF8)<<8; \ (b) = ((buf[1] << 2 ) & 0xF8)<<8; \ buf += 2; \ break; \ \ case VIDEO_PALETTE_RGB24: \ (r) = buf[0] << 8; (g) = buf[1] << 8; \ (b) = buf[2] << 8; \ buf += 3; \ break; \ \ default: \ fprintf(stderr, \ "Format %d not yet supported\n", \ format); \ } \ } barcode_v4l::barcode_v4l() { m_devname = QStringLiteral("/dev/video0"); m_grab_width = 640; m_grab_height = 480; m_fd = -1; - m_buffer = 0; - m_image = 0; + m_buffer = nullptr; + m_image = nullptr; grab_init(); } barcode_v4l::~barcode_v4l() { if (m_fd >= 0) v4l1_close(m_fd); if (m_buffer) delete m_buffer; if (m_image) delete m_image; } bool barcode_v4l::isOpen() { return (m_fd >= 0); } QImage barcode_v4l::grab_one2() { unsigned int bpp = 24, x, y; unsigned int r = 0, g = 0, b = 0; unsigned int src_depth = 16; char *src = m_buffer->data(); static int counter = 0; // adjustment disabled; set to e.g. 20 or 50 to enable the brightness adjustment if (!isOpen()) return QImage(); v4l1_read(m_fd, m_buffer->data(), m_win.width * m_win.height * bpp); if (counter) { long newbright; int f; counter--; f = get_brightness_adj((unsigned char *)m_buffer->data(), m_win.width * m_win.height, &newbright); if (f) { m_pict.brightness += (newbright << 8); myDebug() << "v4l: Adjusting brightness: new brightness " << m_pict.brightness << endl; if (v4l1_ioctl(m_fd, VIDIOCSPICT, &m_pict) == -1) { myDebug() << "v4l: Cannot set brightness." << endl; counter = 0; // do not try again } } else counter = 0; // do not try again } if (m_pict.palette == VIDEO_PALETTE_RGB24) { // optimized case QRgb *scanline; for (y = 0; y < m_win.height; ++y) { scanline = (QRgb*)m_image->scanLine(y); for (x = 0; x < m_win.width; ++x) { const char src1 = *(src++); const char src2 = *(src++); const char src3 = *(src++); scanline[x] = qRgb(src1,src2,src3); } } } else { // generic case for (y = 0; y < m_win.height; ++y) { for (x = 0; x < m_win.width; ++x) { READ_VIDEO_PIXEL(src, m_pict.palette, src_depth, r, g, b); m_image->setPixel( x, y, qRgb(r>>8,g>>8,b>>8) ); } } } return *m_image; } bool barcode_v4l::grab_init() { m_fd = v4l1_open(m_devname.toLatin1().constData(), O_RDONLY); if (m_fd < 0) { myDebug() << "v4l: open " << m_devname << ": " << strerror(errno) << endl; return false; } if (v4l1_ioctl(m_fd, VIDIOCGCAP, &m_capability) < 0) { myDebug() << "v4l: ioctl VIDIOCGCAP failed; " << m_devname << " not a video4linux device?" << endl; v4l1_close(m_fd); m_fd = -1; return false; } if (v4l1_ioctl(m_fd, VIDIOCGWIN, &m_win) < 0) { myDebug() << "v4l: ioctl VIDIOCGWIN failed" << endl; v4l1_close(m_fd); m_fd = -1; return false; } if (v4l1_ioctl(m_fd, VIDIOCGPICT, &m_pict) < 0) { myDebug() << "v4l: ioctl VIDIOCGPICT failed" << endl; v4l1_close(m_fd); m_fd = -1; return false; } if (m_capability.type & VID_TYPE_MONOCHROME) { m_pict.depth=8; m_pict.palette=VIDEO_PALETTE_GREY; /* 8bit grey */ if (v4l1_ioctl(m_fd, VIDIOCSPICT, &m_pict) < 0) { myDebug() << "v4l: Unable to find a supported capture format." << endl; v4l1_close(m_fd); m_fd = -1; return false; } } else { m_pict.depth=24; m_pict.palette=VIDEO_PALETTE_RGB24; if (v4l1_ioctl(m_fd, VIDIOCSPICT, &m_pict) < 0) { myDebug() << "v4l: Unable to find a supported capture format." << endl; v4l1_close(m_fd); m_fd = -1; return false; } } // check the values video_picture temp; v4l1_ioctl(m_fd, VIDIOCGPICT, &temp); if ((temp.depth != m_pict.depth) || (temp.palette != m_pict.palette)) { myDebug() << "v4l: Unable to find a supported capture format." << endl; v4l1_close(m_fd); m_fd = -1; return false; } int bpp = 24; m_buffer = new QByteArray; m_buffer->reserve( m_win.width * m_win.height * bpp ); // FIXME! I think the example from the Linux kernel wastes memory here m_image = new QImage( m_win.width, m_win.height, QImage::Format_RGB32 ); return true; } int barcode_v4l::get_brightness_adj(unsigned char *image, long size, long *brightness) { long i, tot = 0; for (i=0;i= 126 && (tot/(size*3)) <= 130); } diff --git a/src/bibtexkeydialog.cpp b/src/bibtexkeydialog.cpp index 91da0838..2c61a7fd 100644 --- a/src/bibtexkeydialog.cpp +++ b/src/bibtexkeydialog.cpp @@ -1,133 +1,133 @@ /*************************************************************************** Copyright (C) 2011 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 "bibtexkeydialog.h" #include "collections/bibtexcollection.h" #include "entry.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include using Tellico::BibtexKeyDialog; // default button is going to be used as a print button, so it's separated BibtexKeyDialog::BibtexKeyDialog(Data::CollPtr coll_, QWidget* parent_) : QDialog(parent_), m_coll(coll_) { Q_ASSERT(m_coll); setModal(false); setWindowTitle(i18n("Citation Key Manager")); QVBoxLayout* topLayout = new QVBoxLayout; setLayout(topLayout); QWidget* mainWidget = new QWidget(this); topLayout->addWidget(mainWidget); m_dupeLabel = new KTitleWidget(this); m_dupeLabel->setText(m_coll->title(), KTitleWidget::PlainMessage); m_dupeLabel->setComment(i18n("Checking for entries with duplicate citation keys...")); m_dupeLabel->setPixmap(QIcon::fromTheme(QStringLiteral("tools-wizard")).pixmap(64, 64), KTitleWidget::ImageLeft); topLayout->addWidget(m_dupeLabel); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); buttonBox->button(QDialogButtonBox::Close)->setDefault(true); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QPushButton* checkDuplicates = new QPushButton(buttonBox); KGuiItem::assign(checkDuplicates, KGuiItem(i18n("Check for duplicates"), QStringLiteral("system-search"))); buttonBox->addButton(checkDuplicates, QDialogButtonBox::ActionRole); m_filterButton = new QPushButton(buttonBox); KGuiItem::assign(m_filterButton, KGuiItem(i18n("Filter for duplicates"), QStringLiteral("view-filter"))); buttonBox->addButton(m_filterButton, QDialogButtonBox::ActionRole); topLayout->addWidget(buttonBox); if(m_coll->type() != Data::Collection::Bibtex) { // if it's not a bibliography, no need to save a pointer m_coll = Data::CollPtr(); myWarning() << "not a bibliography"; } else { // the button is enabled when duplicates are found m_filterButton->setEnabled(false); - connect(m_filterButton, SIGNAL(clicked()), SLOT(slotFilterDuplicates())); - connect(checkDuplicates, SIGNAL(clicked()), SLOT(slotCheckDuplicates())); - QTimer::singleShot(0, this, SLOT(slotCheckDuplicatesImpl())); + connect(m_filterButton, &QAbstractButton::clicked, this, &BibtexKeyDialog::slotFilterDuplicates); + connect(checkDuplicates, &QAbstractButton::clicked, this, &BibtexKeyDialog::slotCheckDuplicates); + QTimer::singleShot(0, this, &BibtexKeyDialog::slotCheckDuplicatesImpl); } } BibtexKeyDialog::~BibtexKeyDialog() { } void BibtexKeyDialog::slotCheckDuplicates() { if(!m_coll) { return; } m_dupeLabel->setComment(i18n("Checking for entries with duplicate citation keys...")); - QTimer::singleShot(0, this, SLOT(slotCheckDuplicatesImpl())); + QTimer::singleShot(0, this, &BibtexKeyDialog::slotCheckDuplicatesImpl); } void BibtexKeyDialog::slotCheckDuplicatesImpl() { const Data::BibtexCollection* c = static_cast(m_coll.data()); m_dupes = c->duplicateBibtexKeys(); m_filterButton->setEnabled(false); if(m_dupes.isEmpty()) { m_dupeLabel->setComment(i18n("There are no duplicate citation keys.")); m_filterButton->setEnabled(false); } else { m_dupeLabel->setComment(i18np("There is %1 duplicate citation key.", "There are %1 duplicate citation keys.", m_dupes.count())); m_filterButton->setEnabled(true); } } void BibtexKeyDialog::slotFilterDuplicates() { if(!m_coll || m_dupes.isEmpty()) { return; } FilterPtr filter(new Filter(Filter::MatchAny)); QSet keys; foreach(Data::EntryPtr entry, m_dupes) { const QString key = entry->field(QStringLiteral("bibtex-key")); if(!keys.contains(key)) { filter->append(new FilterRule(QStringLiteral("bibtex-key"), key, FilterRule::FuncEquals)); keys << key; } } if(!filter->isEmpty()) { emit signalUpdateFilter(filter); } } diff --git a/src/borrowerdialog.cpp b/src/borrowerdialog.cpp index f76e1238..c1edd55f 100644 --- a/src/borrowerdialog.cpp +++ b/src/borrowerdialog.cpp @@ -1,204 +1,204 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "borrowerdialog.h" #include "document.h" #include "collection.h" #include "../tellico_debug.h" #include #include #include #ifdef HAVE_KABC #ifdef QT_STRICT_ITERATORS #define WAS_STRICT #undef QT_STRICT_ITERATORS #endif #include #include #ifdef WAS_STRICT #define QT_STRICT_ITERATORS #undef WAS_STRICT #endif #endif #include #include #include using Tellico::BorrowerDialog; #ifdef HAVE_KABC BorrowerDialog::Item::Item(QTreeWidget* parent_, const KContacts::Addressee& add_) : QTreeWidgetItem(parent_), m_uid(add_.uid()) { setData(0, Qt::DisplayRole, add_.realName().trimmed()); setData(0, Qt::DecorationRole, QIcon::fromTheme(QLatin1String("kaddressbook"))); } #endif BorrowerDialog::Item::Item(QTreeWidget* parent_, const Tellico::Data::Borrower& bor_) : QTreeWidgetItem(parent_), m_uid(bor_.uid()) { setData(0, Qt::DisplayRole, bor_.name()); setData(0, Qt::DecorationRole, QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico")))); } // default button is going to be used as a print button, so it's separated BorrowerDialog::BorrowerDialog(QWidget* parent_) : QDialog(parent_) { setModal(true); setWindowTitle(i18n("Select Borrower")); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QVBoxLayout *mainLayout = new QVBoxLayout(this); setLayout(mainLayout); QWidget* mainWidget = new QWidget(this); mainLayout->addWidget(mainWidget); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); m_treeWidget = new QTreeWidget(mainWidget); mainLayout->addWidget(m_treeWidget); m_treeWidget->setHeaderLabel(i18n("Name")); m_treeWidget->setRootIsDecorated(false); - connect(m_treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), - SLOT(accept())); - connect(m_treeWidget, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), - SLOT(updateEdit(QTreeWidgetItem*))); + connect(m_treeWidget, &QTreeWidget::itemDoubleClicked, + this, &QDialog::accept); + connect(m_treeWidget, &QTreeWidget::currentItemChanged, + this, &BorrowerDialog::updateEdit); m_lineEdit = new KLineEdit(mainWidget); //krazy:exclude=qclasses mainLayout->addWidget(m_lineEdit); - connect(m_lineEdit->completionObject(), SIGNAL(match(const QString&)), - SLOT(selectItem(const QString&))); + connect(m_lineEdit->completionObject(), &KCompletion::match, + this, &BorrowerDialog::selectItem); m_lineEdit->setFocus(); m_lineEdit->completionObject()->setIgnoreCase(true); mainLayout->addWidget(buttonBox); #ifdef HAVE_KABC // Search for all existing contacts Akonadi::ContactSearchJob* job = new Akonadi::ContactSearchJob(); connect(job, SIGNAL(result(KJob*)), this, SLOT(akonadiSearchResult(KJob*))); #endif populateBorrowerList(); setMinimumWidth(400); } void BorrowerDialog::akonadiSearchResult(KJob* job_) { if(job_->error() != 0) { myDebug() << job_->errorString(); return; } #ifdef HAVE_KABC Akonadi::ContactSearchJob* searchJob = qobject_cast(job_); Q_ASSERT(searchJob); populateBorrowerList(); foreach(const KContacts::Addressee& addressee, searchJob->contacts()) { // skip people with no name const QString name = addressee.realName().trimmed(); if(name.isEmpty()) { continue; } if(m_itemHash.contains(name)) { continue; // if an item already exists with this name } Item* item = new Item(m_treeWidget, addressee); m_itemHash.insert(name, item); m_lineEdit->completionObject()->addItem(name); } #endif m_treeWidget->sortItems(0, Qt::AscendingOrder); } void BorrowerDialog::populateBorrowerList() { m_treeWidget->clear(); m_itemHash.clear(); m_lineEdit->completionObject()->clear(); // Bug 307958 - KContacts::Addressee uids are not constant // so populate the borrower list with the existing borrowers // before adding ones from address book Data::BorrowerList borrowers = Data::Document::self()->collection()->borrowers(); foreach(Data::BorrowerPtr bor, borrowers) { if(bor->name().isEmpty()) { continue; } if(m_itemHash.contains(bor->name())) { continue; // if an item already exists with this name } Item* item = new Item(m_treeWidget, *bor); m_itemHash.insert(bor->name(), item); m_lineEdit->completionObject()->addItem(bor->name()); } m_treeWidget->sortItems(0, Qt::AscendingOrder); } void BorrowerDialog::selectItem(const QString& str_) { if(str_.isEmpty()) { return; } QTreeWidgetItem* item = m_itemHash.value(str_); if(item) { m_treeWidget->blockSignals(true); m_treeWidget->setCurrentItem(item); m_treeWidget->scrollToItem(item); m_treeWidget->blockSignals(false); } } void BorrowerDialog::updateEdit(QTreeWidgetItem* item_) { QString s = item_->data(0, Qt::DisplayRole).toString(); m_lineEdit->setText(s); m_lineEdit->setSelection(0, s.length()); m_uid = static_cast(item_)->uid(); } Tellico::Data::BorrowerPtr BorrowerDialog::borrower() { return Data::BorrowerPtr(new Data::Borrower(m_lineEdit->text(), m_uid)); } // static Tellico::Data::BorrowerPtr BorrowerDialog::getBorrower(QWidget* parent_) { BorrowerDialog dlg(parent_); if(dlg.exec() == QDialog::Accepted) { return dlg.borrower(); } return Data::BorrowerPtr(); } diff --git a/src/collectionfieldsdialog.cpp b/src/collectionfieldsdialog.cpp index 293e425c..d1f387ca 100644 --- a/src/collectionfieldsdialog.cpp +++ b/src/collectionfieldsdialog.cpp @@ -1,1091 +1,1094 @@ /*************************************************************************** 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 . * * * ***************************************************************************/ #include "collectionfieldsdialog.h" #include "collection.h" #include "field.h" #include "fieldformat.h" #include "collectionfactory.h" #include "gui/listwidgetitem.h" #include "gui/stringmapdialog.h" #include "gui/combobox.h" #include "tellico_kernel.h" #include "translators/tellico_xml.h" #include "utils/string_utils.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::FieldListItem; using Tellico::CollectionFieldsDialog; class Tellico::FieldListItem : public Tellico::GUI::ListWidgetItem { public: FieldListItem(QListWidget* parent_, Data::FieldPtr field_) : GUI::ListWidgetItem(field_->title(), parent_), m_field(field_) {} Data::FieldPtr field() const { return m_field; } void setField(Data::FieldPtr field) { m_field = field; } private: Data::FieldPtr m_field; }; CollectionFieldsDialog::CollectionFieldsDialog(Tellico::Data::CollPtr coll_, QWidget* parent_) : QDialog(parent_), m_coll(coll_), m_defaultCollection(nullptr), m_currentField(nullptr), m_modified(false), m_updatingValues(false), m_reordered(false), m_oldIndex(-1), m_notifyMode(NotifyKernel) { setModal(false); setWindowTitle(i18n("Collection Fields")); QVBoxLayout* mainLayout = new QVBoxLayout(); setLayout(mainLayout); QWidget* page = new QWidget(this); mainLayout->addWidget(page); QBoxLayout* topLayout = new QHBoxLayout(page); page->setLayout(topLayout); QGroupBox* fieldsGroup = new QGroupBox(i18n("Current Fields"), page); QBoxLayout* fieldsLayout = new QVBoxLayout(fieldsGroup); topLayout->addWidget(fieldsGroup, 1); m_fieldsWidget = new QListWidget(fieldsGroup); m_fieldsWidget->setMinimumWidth(150); fieldsLayout->addWidget(m_fieldsWidget); Data::FieldList fields = m_coll->fields(); foreach(Data::FieldPtr field, fields) { // ignore fields which are not user-editable if(!field->hasFlag(Data::Field::NoEdit)) { (void) new FieldListItem(m_fieldsWidget, field); } } - connect(m_fieldsWidget, SIGNAL(currentRowChanged(int)), SLOT(slotHighlightedChanged(int))); + connect(m_fieldsWidget, &QListWidget::currentRowChanged, this, &CollectionFieldsDialog::slotHighlightedChanged); QWidget* hb1 = new QWidget(fieldsGroup); QHBoxLayout* hb1HBoxLayout = new QHBoxLayout(hb1); hb1HBoxLayout->setMargin(0); fieldsLayout->addWidget(hb1); m_btnNew = new QPushButton(i18nc("New Field", "&New"), hb1); hb1HBoxLayout->addWidget(m_btnNew); m_btnNew->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_btnNew->setWhatsThis(i18n("Add a new field to the collection")); m_btnDelete = new QPushButton(i18nc("Delete Field", "Delete"), hb1); hb1HBoxLayout->addWidget(m_btnDelete); m_btnDelete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); m_btnDelete->setWhatsThis(i18n("Remove a field from the collection")); - connect(m_btnNew, SIGNAL(clicked()), SLOT(slotNew())); - connect(m_btnDelete, SIGNAL(clicked()), SLOT(slotDelete())); + connect(m_btnNew, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotNew); + connect(m_btnDelete, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotDelete); QWidget* hb2 = new QWidget(fieldsGroup); QHBoxLayout* hb2HBoxLayout = new QHBoxLayout(hb2); hb2HBoxLayout->setMargin(0); fieldsLayout->addWidget(hb2); m_btnUp = new QPushButton(hb2); hb2HBoxLayout->addWidget(m_btnUp); m_btnUp->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_btnUp->setWhatsThis(i18n("Move this field up in the list. The list order is important " "for the layout of the entry editor.")); m_btnDown = new QPushButton(hb2); hb2HBoxLayout->addWidget(m_btnDown); m_btnDown->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_btnDown->setWhatsThis(i18n("Move this field down in the list. The list order is important " "for the layout of the entry editor.")); - connect(m_btnUp, SIGNAL(clicked()), SLOT(slotMoveUp())); - connect(m_btnDown, SIGNAL(clicked()), SLOT(slotMoveDown())); + connect(m_btnUp, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotMoveUp); + connect(m_btnDown, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotMoveDown); QWidget* vbox = new QWidget(page); QVBoxLayout* vboxVBoxLayout = new QVBoxLayout(vbox); vboxVBoxLayout->setMargin(0); topLayout->addWidget(vbox, 2); QGroupBox* propGroup = new QGroupBox(i18n("Field Properties"), vbox); vboxVBoxLayout->addWidget(propGroup); QBoxLayout* propLayout = new QVBoxLayout(propGroup); QWidget* grid = new QWidget(propGroup); QGridLayout* layout = new QGridLayout(grid); propLayout->addWidget(grid); int row = -1; QLabel* label = new QLabel(i18n("&Title:"), grid); layout->addWidget(label, ++row, 0); m_titleEdit = new QLineEdit(grid); layout->addWidget(m_titleEdit, row, 1); label->setBuddy(m_titleEdit); QString whats = i18n("The title of the field"); label->setWhatsThis(whats); m_titleEdit->setWhatsThis(whats); - connect(m_titleEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_titleEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("T&ype:"), grid); layout->addWidget(label, row, 2); m_typeCombo = new KComboBox(grid); layout->addWidget(m_typeCombo, row, 3); label->setBuddy(m_typeCombo); whats = QStringLiteral(""); whats += i18n("The type of the field determines what values may be used. "); whats += i18n("Simple Text is used for most fields. "); whats += i18n("Paragraph is for large text blocks. "); whats += i18n("Choice limits the field to certain values. "); whats += i18n("Checkbox is for a simple yes/no value. "); whats += i18n("Number indicates that the field contains a numerical value. "); whats += i18n("URL is for fields which refer to URLs, including references to other files. "); whats += i18n("A Table may hold one or more columns of values. "); whats += i18n("An Image field holds a picture. "); whats += i18n("A Date field can be used for values with a day, month, and year. "); whats += i18n("A Rating field uses stars to show a rating number. "); whats += QLatin1String(""); label->setWhatsThis(whats); m_typeCombo->setWhatsThis(whats); // the typeTitles match the fieldMap().values() but in a better order m_typeCombo->addItems(Data::Field::typeTitles()); - connect(m_typeCombo, SIGNAL(activated(int)), SLOT(slotModified())); - connect(m_typeCombo, SIGNAL(activated(const QString&)), SLOT(slotTypeChanged(const QString&))); + void (QComboBox::* activatedInt)(int) = &QComboBox::activated; + connect(m_typeCombo, activatedInt, this, &CollectionFieldsDialog::slotModified); + void (QComboBox::* activatedString)(const QString&) = &QComboBox::activated; + connect(m_typeCombo, activatedString, this, &CollectionFieldsDialog::slotTypeChanged); label = new QLabel(i18n("Cate&gory:"), grid); layout->addWidget(label, ++row, 0); m_catCombo = new KComboBox(true, grid); layout->addWidget(m_catCombo, row, 1); label->setBuddy(m_catCombo); whats = i18n("The field category determines where the field is placed in the editor."); label->setWhatsThis(whats); m_catCombo->setWhatsThis(whats); // I don't want to include the categories for singleCategory fields QStringList cats; const QStringList allCats = m_coll->fieldCategories(); foreach(const QString& cat, allCats) { Data::FieldList fields = m_coll->fieldsByCategory(cat); if(!fields.isEmpty() && !fields.at(0)->isSingleCategory()) { cats.append(cat); } } m_catCombo->addItems(cats); m_catCombo->setDuplicatesEnabled(false); - connect(m_catCombo, SIGNAL(currentTextChanged(const QString&)), SLOT(slotModified())); + connect(m_catCombo, &QComboBox::currentTextChanged, this, &CollectionFieldsDialog::slotModified); m_btnExtended = new QPushButton(i18n("Set &properties..."), grid); m_btnExtended->setIcon(QIcon::fromTheme(QStringLiteral("bookmarks"))); layout->addWidget(m_btnExtended, row, 2, 1, 2); label->setBuddy(m_btnExtended); whats = i18n("Extended field properties are used to specify things such as the corresponding bibtex field."); label->setWhatsThis(whats); m_btnExtended->setWhatsThis(whats); - connect(m_btnExtended, SIGNAL(clicked()), SLOT(slotShowExtendedProperties())); + connect(m_btnExtended, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotShowExtendedProperties); label = new QLabel(i18n("Description:"), grid); layout->addWidget(label, ++row, 0); m_descEdit = new QLineEdit(grid); m_descEdit->setMinimumWidth(150); layout->addWidget(m_descEdit, row, 1, 1, 3); label->setBuddy(m_descEdit); whats = i18n("The description is a useful reminder of what information is contained in the field."); label->setWhatsThis(whats); m_descEdit->setWhatsThis(whats); - connect(m_descEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_descEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); QGroupBox* valueGroup = new QGroupBox(i18n("Value Options"), vbox); vboxVBoxLayout->addWidget(valueGroup); QGridLayout* valueLayout = new QGridLayout(valueGroup); int valueRow = -1; label = new QLabel(i18n("Default value:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_defaultEdit = new QLineEdit(valueGroup); valueLayout->addWidget(m_defaultEdit, valueRow, 1, 1, 3); label->setBuddy(m_defaultEdit); whats = i18n("A default value can be set for new entries."); label->setWhatsThis(whats); m_defaultEdit->setWhatsThis(whats); - connect(m_defaultEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_defaultEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("Value template:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_derivedEdit = new QLineEdit(valueGroup); m_derivedEdit->setMinimumWidth(150); valueLayout->addWidget(m_derivedEdit, valueRow, 1); label->setBuddy(m_derivedEdit); /* TRANSLATORS: Do not translate %{year} and %{title}. */ whats = i18n("Derived values are formed from the values of other fields according to the value template. " "Named fields, such as \"%{year} %{title}\", get substituted in the value."); label->setWhatsThis(whats); m_derivedEdit->setWhatsThis(whats); - connect(m_derivedEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_derivedEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); m_derived = new QCheckBox(i18n("Use derived value"), valueGroup); m_derived->setWhatsThis(whats); valueLayout->addWidget(m_derived, valueRow, 2, 1, 2); - connect(m_derived, SIGNAL(clicked(bool)), SLOT(slotDerivedChecked(bool))); - connect(m_derived, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_derived, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotDerivedChecked); + connect(m_derived, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("A&llowed values:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_allowEdit = new QLineEdit(valueGroup); valueLayout->addWidget(m_allowEdit, valueRow, 1, 1, 3); label->setBuddy(m_allowEdit); whats = i18n("For Choice-type fields, these are the only values allowed. They are " "placed in a combo box. The possible values have to be separated by a semi-colon, " "for example: \"dog; cat; mouse\""); label->setWhatsThis(whats); m_allowEdit->setWhatsThis(whats); - connect(m_allowEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_allowEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("Format options:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_formatCombo = new GUI::ComboBox(valueGroup); valueLayout->addWidget(m_formatCombo, valueRow, 1, 1, 3); label->setBuddy(m_formatCombo); m_formatCombo->addItem(i18n("No formatting"), FieldFormat::FormatNone); m_formatCombo->addItem(i18n("Allow auto-capitalization only"), FieldFormat::FormatPlain); m_formatCombo->addItem(i18n("Format as a title"), FieldFormat::FormatTitle); m_formatCombo->addItem(i18n("Format as a name"), FieldFormat::FormatName); - connect(m_formatCombo, SIGNAL(currentIndexChanged(int)), SLOT(slotModified())); + void (QComboBox::* currentIndexChanged)(int) = &QComboBox::currentIndexChanged; + connect(m_formatCombo, currentIndexChanged, this, &CollectionFieldsDialog::slotModified); QGroupBox* optionsGroup = new QGroupBox(i18n("Field Options"), vbox); vboxVBoxLayout->addWidget(optionsGroup); QBoxLayout* optionsLayout = new QVBoxLayout(optionsGroup); m_complete = new QCheckBox(i18n("Enable auto-completion"), optionsGroup); m_complete->setWhatsThis(i18n("If checked, KDE auto-completion will be enabled in the " "text edit box for this field.")); m_multiple = new QCheckBox(i18n("Allow multiple values"), optionsGroup); m_multiple->setWhatsThis(i18n("If checked, Tellico will parse the values in the field " "for multiple values, separated by a semi-colon.")); m_grouped = new QCheckBox(i18n("Allow grouping"), optionsGroup); m_grouped->setWhatsThis(i18n("If checked, this field may be used to group the entries in " "the group view.")); optionsLayout->addWidget(m_complete); optionsLayout->addWidget(m_multiple); optionsLayout->addWidget(m_grouped); - connect(m_complete, SIGNAL(clicked()), SLOT(slotModified())); - connect(m_multiple, SIGNAL(clicked()), SLOT(slotModified())); - connect(m_grouped, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_complete, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); + connect(m_multiple, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); + connect(m_grouped, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); // need to stretch at bottom vboxVBoxLayout->addStretch(1); // keep a default collection m_defaultCollection = CollectionFactory::collection(m_coll->type(), true); m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok| QDialogButtonBox::Cancel| QDialogButtonBox::Help| QDialogButtonBox::RestoreDefaults| QDialogButtonBox::Apply); mainLayout->addWidget(m_buttonBox); QPushButton* okButton = m_buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - connect(m_buttonBox, SIGNAL(helpRequested()), this, SLOT(slotHelp())); + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_buttonBox, &QDialogButtonBox::helpRequested, this, &CollectionFieldsDialog::slotHelp); m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setWhatsThis(i18n("Revert the selected field's properties to the default values.")); - connect(okButton, SIGNAL(clicked()), SLOT(slotOk())); - connect(m_buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); - connect(m_buttonBox->button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked()), SLOT(slotDefault())); + connect(okButton, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotOk); + connect(m_buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotApply); + connect(m_buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotDefault); okButton->setEnabled(false); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); // initially the m_typeCombo is populated with all types, but as soon as something is // selected in the fields box, the combo box is cleared and filled with the allowable // new types. The problem is that when more types are added, the size of the combo box // doesn't change. So when everything is laid out, the combo box needs to have all the // items there. - QTimer::singleShot(0, this, SLOT(slotSelectInitial())); + QTimer::singleShot(0, this, &CollectionFieldsDialog::slotSelectInitial); } CollectionFieldsDialog::~CollectionFieldsDialog() { } void CollectionFieldsDialog::setNotifyKernel(bool notify_) { if(notify_) { m_notifyMode = NotifyKernel; } else { m_notifyMode = NoNotification; } } void CollectionFieldsDialog::slotSelectInitial() { // the accel management is here so that it doesn't cause conflicts with the // ones explicitly set in the constructor KAcceleratorManager::manage(this); m_fieldsWidget->setCurrentRow(0); } void CollectionFieldsDialog::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("fields-dialog")); } void CollectionFieldsDialog::slotOk() { slotApply(); accept(); } void CollectionFieldsDialog::slotApply() { updateField(); if(!checkValues()) { return; } applyChanges(); } void CollectionFieldsDialog::applyChanges() { // start a command group, "Modify" is a generic term here since the commands could be add, modify, or delete if(m_notifyMode == NotifyKernel) { Kernel::self()->beginCommandGroup(i18n("Modify Fields")); } foreach(Data::FieldPtr field, m_copiedFields) { // check for Choice fields with removed values to warn user if(field->type() == Data::Field::Choice || field->type() == Data::Field::Rating) { QStringList oldValues = m_coll->fieldByName(field->name())->allowed(); QStringList newValues = field->allowed(); for(QStringList::ConstIterator vIt = oldValues.constBegin(); vIt != oldValues.constEnd(); ++vIt) { if(newValues.contains(*vIt)) { continue; } int ret = KMessageBox::warningContinueCancel(this, i18n("Removing allowed values from the %1 field which " "currently exist in the collection may cause data corruption. " "Do you want to keep your modified values or cancel and revert " "to the current ones?", field->title()), QString(), KGuiItem(i18n("Keep modified values"))); if(ret != KMessageBox::Continue) { if(field->type() == Data::Field::Choice) { field->setAllowed(oldValues); } else { // rating field Data::FieldPtr oldField = m_coll->fieldByName(field->name()); field->setProperty(QStringLiteral("minimum"), oldField->property(QStringLiteral("minimum"))); field->setProperty(QStringLiteral("maximum"), oldField->property(QStringLiteral("maximum"))); } } break; } } if(m_notifyMode == NotifyKernel) { Kernel::self()->modifyField(field); } else { m_coll->modifyField(field); } } foreach(Data::FieldPtr field, m_newFields) { if(m_notifyMode == NotifyKernel) { Kernel::self()->addField(field); } else { m_coll->addField(field); } } // set all text not to be colored, and get new list Data::FieldList fields; fields.reserve(m_fieldsWidget->count()); for(int i = 0; i < m_fieldsWidget->count(); ++i) { FieldListItem* item = static_cast(m_fieldsWidget->item(i)); item->setColored(false); if(m_reordered) { Data::FieldPtr field = item->field(); if(field) { fields.append(field); } } } // if reordering fields, need to add ReadOnly fields since they were not shown if(m_reordered) { foreach(Data::FieldPtr field, m_coll->fields()) { if(field->hasFlag(Data::Field::NoEdit)) { fields.append(field); } } } if(fields.count() > 0) { if(m_notifyMode == NotifyKernel) { Kernel::self()->reorderFields(fields); } else { m_coll->reorderFields(fields); } } // commit command group if(m_notifyMode == NotifyKernel) { Kernel::self()->endCommandGroup(); } // now clear copied fields m_copiedFields.clear(); // clear new ones, too m_newFields.clear(); m_currentField = static_cast(m_fieldsWidget->currentItem())->field(); // the field type might have changed, so need to update the type combo list with possible values if(m_currentField) { // set the updating flag since the values are changing and slots are firing // but we don't care about UI indications of changes bool wasUpdating = m_updatingValues; m_updatingValues = true; QString currType = m_typeCombo->currentText(); m_typeCombo->clear(); m_typeCombo->addItems(newTypesAllowed(m_currentField->type())); m_typeCombo->setCurrentItem(currType); // template might have been changed for dependent fields m_derivedEdit->setText(m_currentField->property(QStringLiteral("template"))); m_updatingValues = wasUpdating; } m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } void CollectionFieldsDialog::slotNew() { // first update the current one with all the values from the edit widgets updateField(); // next check old values if(!checkValues()) { return; } QString name = QLatin1String("custom") + QString::number(m_newFields.count()+1); int count = m_newFields.count() + 1; QString title = i18n("New Field %1", count); while(!m_fieldsWidget->findItems(title, Qt::MatchExactly).isEmpty()) { ++count; title = i18n("New Field %1", count); } Data::FieldPtr field(new Data::Field(name, title)); m_newFields.append(field); // myDebug() << "adding new field " << title; m_currentField = field; FieldListItem* item = new FieldListItem(m_fieldsWidget, field); item->setColored(true); m_fieldsWidget->setCurrentItem(item); m_fieldsWidget->scrollToItem(item); slotModified(); m_titleEdit->setFocus(); m_titleEdit->selectAll(); } void CollectionFieldsDialog::slotDelete() { if(!m_currentField) { return; } if(m_newFields.contains(m_currentField)) { // remove field from vector before deleting item containing field m_newFields.removeAll(m_currentField); } else { if(m_notifyMode == NotifyKernel) { if(!Kernel::self()->removeField(m_currentField)) { return; } } else { m_coll->removeField(m_currentField); } emit signalCollectionModified(); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); } int currentRow = m_fieldsWidget->currentRow(); delete m_fieldsWidget->takeItem(currentRow); m_fieldsWidget->setCurrentRow(qMin(currentRow, m_fieldsWidget->count()-1)); m_fieldsWidget->scrollToItem(m_fieldsWidget->currentItem()); m_currentField = static_cast(m_fieldsWidget->currentItem())->field(); // QSharedData gets auto-deleted } void CollectionFieldsDialog::slotTypeChanged(const QString& type_) { Data::Field::Type type = Data::Field::Undef; const Data::Field::FieldMap fieldMap = Data::Field::typeMap(); for(Data::Field::FieldMap::ConstIterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { if(it.value() == type_) { type = it.key(); break; } } if(type == Data::Field::Undef) { myWarning() << "type name not recognized:" << type_; type = Data::Field::Line; } // only choice types gets allowed values m_allowEdit->setEnabled(type == Data::Field::Choice); // paragraphs, tables, and images are their own category bool isCategory = (type == Data::Field::Para || type == Data::Field::Table || type == Data::Field::Image); m_catCombo->setEnabled(!isCategory); // formatting is only applicable when the type is simple text or a table bool isText = (type == Data::Field::Line || type == Data::Field::Table); // formatNone is the default m_formatCombo->setEnabled(isText); // multiple is only applicable for simple text and number isText = (type == Data::Field::Line || type == Data::Field::Number); m_multiple->setEnabled(isText); // completion is only applicable for simple text, number, and URL isText = (isText || type == Data::Field::URL); m_complete->setEnabled(isText); // grouping is not possible with paragraphs or images m_grouped->setEnabled(type != Data::Field::Para && type != Data::Field::Image); } void CollectionFieldsDialog::slotHighlightedChanged(int index_) { if(index_ == m_oldIndex) { return; // state when checkValues() returns false } // use this instead of blocking signals everywhere m_updatingValues = true; // first update the current one with all the values from the edit widgets updateField(); // next check old values if(!checkValues()) { // Other functions get called and change selection after this one. Use a SingleShot to revert - QTimer::singleShot(0, this, SLOT(resetToCurrent())); + QTimer::singleShot(0, this, &CollectionFieldsDialog::resetToCurrent); m_updatingValues = false; return; } m_oldIndex = index_; m_btnUp->setEnabled(index_ > 0); m_btnDown->setEnabled(index_ < static_cast(m_fieldsWidget->count())-1); FieldListItem* item = dynamic_cast(m_fieldsWidget->item(index_)); if(!item) { return; } // need to get a pointer to the field with the new values to insert Data::FieldPtr field = item->field(); if(!field) { myDebug() << "no field found!"; return; } // type is limited to certain types, unless it's a new field m_typeCombo->clear(); if(m_newFields.contains(field)) { m_typeCombo->addItems(newTypesAllowed(Data::Field::Undef)); } else { m_typeCombo->addItems(newTypesAllowed(field->type())); } populate(field); // default button is enabled only if default collection contains the field if(m_defaultCollection) { const bool hasField = m_defaultCollection->hasField(field->name()); m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(hasField); } m_currentField = field; m_updatingValues = false; } void CollectionFieldsDialog::updateField() { Data::FieldPtr field = m_currentField; if(!field || !m_modified) { return; } // only update name if it's one of the new ones if(m_newFields.contains(field)) { // name needs to be a valid XML element name QString name = XML::elementName(m_titleEdit->text().toLower()); if(name.isEmpty()) { // might end up with empty string name = QLatin1String("custom") + QString::number(m_newFields.count()+1); } while(m_coll->hasField(name)) { // ensure name uniqueness name += QLatin1String("-new"); } field->setName(name); } const QString title = m_titleEdit->text().simplified(); updateTitle(title); const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); for(Data::Field::FieldMap::ConstIterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { if(it.value() == m_typeCombo->currentText()) { field->setType(it.key()); break; } } if(field->type() == Data::Field::Choice) { const QRegExp rx(QLatin1String("\\s*;\\s*")); field->setAllowed(m_allowEdit->text().split(rx, QString::SkipEmptyParts)); field->setProperty(QStringLiteral("minimum"), QString()); field->setProperty(QStringLiteral("maximum"), QString()); } else if(field->type() == Data::Field::Rating) { QString v = field->property(QStringLiteral("minimum")); if(v.isEmpty()) { field->setProperty(QStringLiteral("minimum"), QString::number(1)); } v = field->property(QStringLiteral("maximum")); if(v.isEmpty()) { field->setProperty(QStringLiteral("maximum"), QString::number(5)); } } if(field->isSingleCategory()) { field->setCategory(field->title()); } else { QString category = m_catCombo->currentText().simplified(); field->setCategory(category); m_catCombo->setCurrentItem(category, true); // if it doesn't exist, it's added } if(m_derived->isChecked()) { field->setProperty(QStringLiteral("template"), m_derivedEdit->text()); } field->setDescription(m_descEdit->text()); field->setDefaultValue(m_defaultEdit->text()); if(m_formatCombo->isEnabled()) { field->setFormatType(static_cast(m_formatCombo->currentData().toInt())); } else { field->setFormatType(FieldFormat::FormatNone); } int flags = 0; if(m_derived->isChecked()) { flags |= Data::Field::Derived; } if(m_complete->isChecked()) { flags |= Data::Field::AllowCompletion; } if(m_grouped->isChecked()) { flags |= Data::Field::AllowGrouped; } if(m_multiple->isChecked()) { flags |= Data::Field::AllowMultiple; } field->setFlags(flags); m_modified = false; } // The purpose here is to first set the modified flag. Then, if the field being edited is one // that exists in the collection already, a deep copy needs to be made. void CollectionFieldsDialog::slotModified() { // if I'm just updating the values, I don't care if(m_updatingValues) { return; } m_modified = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); if(!m_currentField) { myDebug() << "no current field!"; m_currentField = static_cast(m_fieldsWidget->currentItem())->field(); } // color the text static_cast(m_fieldsWidget->currentItem())->setColored(true); // check if copy exists already if(m_copiedFields.contains(m_currentField)) { return; } // or, check if is a new field, in which case no copy is needed // check if copy exists already if(m_newFields.contains(m_currentField)) { return; } m_currentField = new Data::Field(*m_currentField); m_copiedFields.append(m_currentField); static_cast(m_fieldsWidget->currentItem())->setField(m_currentField); } void CollectionFieldsDialog::updateTitle(const QString& title_) { if(m_currentField && m_currentField->title() != title_) { m_fieldsWidget->blockSignals(true); FieldListItem* oldItem = findItem(m_currentField); if(!oldItem) { return; } oldItem->setText(title_); // will always be colored since it's new oldItem->setColored(true); m_currentField->setTitle(title_); m_fieldsWidget->blockSignals(false); } } void CollectionFieldsDialog::slotDefault() { if(!m_currentField) { return; } Data::FieldPtr defaultField = m_defaultCollection->fieldByName(m_currentField->name()); if(!defaultField) { return; } QString caption = i18n("Revert Field Properties"); QString text = i18n("

Do you really want to revert the properties for the %1 " "field back to their default values?

", m_currentField->title()); QString dontAsk = QStringLiteral("RevertFieldProperties"); int ret = KMessageBox::warningContinueCancel(this, text, caption, KGuiItem(i18n("Revert")), KStandardGuiItem::cancel(), dontAsk); if(ret != KMessageBox::Continue) { return; } // now update all values with default m_updatingValues = true; populate(defaultField); m_updatingValues = false; slotModified(); } void CollectionFieldsDialog::slotMoveUp() { int idx = m_fieldsWidget->currentRow(); if(idx < 1) { return; } // takeItem ends up signalling that the current index changed // need to revert m_oldIndex after taking the item QListWidgetItem* item = m_fieldsWidget->takeItem(idx); m_oldIndex++; m_fieldsWidget->insertItem(idx-1, item); m_fieldsWidget->setCurrentItem(item); m_reordered = true; // don't call slotModified() since that creates a deep copy. m_modified = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); } void CollectionFieldsDialog::slotMoveDown() { int idx = m_fieldsWidget->currentRow(); if(idx > m_fieldsWidget->count()-1) { return; } // takeItem ends up signalling that the current index changed // need to revert m_oldIndex after taking the item QListWidgetItem* item = m_fieldsWidget->takeItem(idx); m_oldIndex--; m_fieldsWidget->insertItem(idx+1, item); m_fieldsWidget->setCurrentItem(item); m_reordered = true; // don't call slotModified() since that creates a deep copy. m_modified = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); } Tellico::FieldListItem* CollectionFieldsDialog::findItem(Tellico::Data::FieldPtr field_) { for(int i = 0; i < m_fieldsWidget->count(); ++i) { FieldListItem* textItem = static_cast(m_fieldsWidget->item(i)); if(textItem->field() == field_) { return textItem; } } return nullptr; } bool CollectionFieldsDialog::slotShowExtendedProperties() { if(!m_currentField) { return false; } // the default value is included in properties, but it has a // separate edit box QString dv = m_currentField->defaultValue(); QString dt = m_currentField->property(QStringLiteral("template")); StringMap props = m_currentField->propertyList(); props.remove(QStringLiteral("default")); props.remove(QStringLiteral("template")); StringMapDialog dlg(props, this, true); dlg.setWindowTitle(i18n("Extended Field Properties")); dlg.setLabels(i18n("Property"), i18n("Value")); if(dlg.exec() == QDialog::Accepted) { props = dlg.stringMap(); if(!dv.isEmpty()) { props.insert(QStringLiteral("default"), dv); } if(!dt.isEmpty()) { props.insert(QStringLiteral("template"), dt); } m_currentField->setPropertyList(props); slotModified(); return true; } return false; } void CollectionFieldsDialog::slotDerivedChecked(bool checked_) { m_defaultEdit->setEnabled(!checked_); m_derivedEdit->setEnabled(checked_); } void CollectionFieldsDialog::resetToCurrent() { m_fieldsWidget->setCurrentRow(m_oldIndex); } bool CollectionFieldsDialog::checkValues() { if(!m_currentField) { return true; } const QString title = m_currentField->title(); // find total number of boxes with this title in case multiple new ones with same title were added int titleCount = m_fieldsWidget->findItems(title, Qt::MatchExactly).count(); if((m_coll->fieldByTitle(title) && m_coll->fieldNameByTitle(title) != m_currentField->name()) || titleCount > 1) { // already have a field with this title KMessageBox::sorry(this, i18n("A field with this title already exists. Please enter a different title.")); m_titleEdit->selectAll(); return false; } const QString category = m_currentField->category(); if(category.isEmpty()) { KMessageBox::sorry(this, i18n("The category may not be empty. Please enter a category.")); m_catCombo->lineEdit()->selectAll(); return false; } Data::FieldList fields = m_coll->fieldsByCategory(category); if(!fields.isEmpty() && fields[0]->isSingleCategory() && fields[0]->name() != m_currentField->name()) { // can't have this category, cause it conflicts with a single-category field KMessageBox::sorry(this, i18n("A field may not be in the same category as a Paragraph, " "Table or Image field. Please enter a different category.")); m_catCombo->lineEdit()->selectAll(); return false; } // the combobox is disabled for single-category fields if(!m_catCombo->isEnabled() && m_coll->fieldByTitle(title) && m_coll->fieldNameByTitle(title) != m_currentField->name()) { KMessageBox::sorry(this, i18n("A field's title may not be the same as an existing category. " "Please enter a different title.")); m_titleEdit->selectAll(); return false; } // check for rating values outside bounds if(m_currentField->type() == Data::Field::Rating) { bool ok; // ok to ignore this here int low = Tellico::toUInt(m_currentField->property(QStringLiteral("minimum")), &ok); int high = Tellico::toUInt(m_currentField->property(QStringLiteral("maximum")), &ok); while(low < 1 || low > 9 || high < 1 || high > 10 || low >= high) { KMessageBox::sorry(this, i18n("The range for a rating field must be between 1 and 10, " "and the lower bound must be less than the higher bound. " "Please enter different low and high properties.")); if(slotShowExtendedProperties()) { low = Tellico::toUInt(m_currentField->property(QStringLiteral("minimum")), &ok); high = Tellico::toUInt(m_currentField->property(QStringLiteral("maximum")), &ok); } else { return false; } } } else if(m_currentField->type() == Data::Field::Table) { bool ok; // ok to ignore this here int ncols = Tellico::toUInt(m_currentField->property(QStringLiteral("columns")), &ok); // also enforced in GUI::TableFieldWidget if(ncols > 10) { KMessageBox::sorry(this, i18n("Tables are limited to a maximum of ten columns.")); m_currentField->setProperty(QStringLiteral("columns"), QStringLiteral("10")); } } if(m_derived->isChecked() && !m_derivedEdit->text().contains(QLatin1Char('%'))) { KMessageBox::sorry(this, i18n("A field with a derived value must have a value template.")); m_derivedEdit->setFocus(); m_derivedEdit->selectAll(); return false; } return true; } void CollectionFieldsDialog::populate(Data::FieldPtr field_) { m_titleEdit->setText(field_->title()); // if the current name is not there, then this will change the list! const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); int idx = m_typeCombo->findText(fieldMap[field_->type()]); m_typeCombo->setCurrentIndex(idx); slotTypeChanged(fieldMap[field_->type()]); // just setting the text doesn't emit the activated signal if(field_->type() == Data::Field::Choice) { m_allowEdit->setText(field_->allowed().join(FieldFormat::delimiterString())); } else { m_allowEdit->clear(); } idx = m_catCombo->findText(field_->category()); if(idx > -1) { m_catCombo->setCurrentIndex(idx); // have to do this here } else { m_catCombo->lineEdit()->setText(field_->category()); } m_descEdit->setText(field_->description()); if(field_->hasFlag(Data::Field::Derived)) { m_derivedEdit->setText(field_->property(QStringLiteral("template"))); m_derived->setChecked(true); m_defaultEdit->clear(); } else { m_derivedEdit->clear(); m_derived->setChecked(false); m_defaultEdit->setText(field_->defaultValue()); } slotDerivedChecked(m_derived->isChecked()); m_formatCombo->setCurrentData(field_->formatType()); m_complete->setChecked(field_->hasFlag(Data::Field::AllowCompletion)); m_multiple->setChecked(field_->hasFlag(Data::Field::AllowMultiple)); m_grouped->setChecked(field_->hasFlag(Data::Field::AllowGrouped)); m_btnDelete->setEnabled(!field_->hasFlag(Data::Field::NoDelete)); } // only certain type changes are allowed QStringList CollectionFieldsDialog::newTypesAllowed(int type_ /*=0*/) { // Undef means return all if(type_ == Data::Field::Undef) { return Data::Field::typeTitles(); } const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); QStringList newTypes; switch(type_) { case Data::Field::Line: // might not work if converted to a number or URL, but ok case Data::Field::Number: case Data::Field::URL: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::URL]; newTypes += fieldMap[Data::Field::Table]; break; case Data::Field::Date: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Date]; break; case Data::Field::Bool: // doesn't really make sense, but can't hurt newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; newTypes += fieldMap[Data::Field::Bool]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::URL]; newTypes += fieldMap[Data::Field::Table]; break; case Data::Field::Choice: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; newTypes += fieldMap[Data::Field::Choice]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::URL]; newTypes += fieldMap[Data::Field::Table]; newTypes += fieldMap[Data::Field::Rating]; break; case Data::Field::Table: // not really a good idea since the row delimiter will be exposed, but allow it case Data::Field::Table2: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::Table]; break; case Data::Field::Para: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; break; case Data::Field::Rating: newTypes += fieldMap[Data::Field::Choice]; newTypes += fieldMap[Data::Field::Rating]; break; // these can never be changed case Data::Field::Image: newTypes += fieldMap[static_cast(type_)]; break; default: myDebug() << "no match for " << type_; newTypes = Data::Field::typeTitles(); break; } return newTypes; } diff --git a/src/configdialog.cpp b/src/configdialog.cpp index da9ae1a1..dbf5ebff 100644 --- a/src/configdialog.cpp +++ b/src/configdialog.cpp @@ -1,1174 +1,1173 @@ /*************************************************************************** Copyright (C) 2001-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 . * * * ***************************************************************************/ #include #include "configdialog.h" #include "field.h" #include "collection.h" #include "collectionfactory.h" #include "fetch/execexternalfetcher.h" #include "fetch/fetchmanager.h" #include "fetch/configwidget.h" #include "controller.h" #include "fetcherconfigdialog.h" #include "tellico_kernel.h" #include "utils/tellico_utils.h" #include "utils/string_utils.h" #include "config/tellico_config.h" #include "images/imagefactory.h" #include "gui/combobox.h" #include "gui/collectiontypecombo.h" #include "gui/previewdialog.h" #include "newstuff/manager.h" #include "fieldformat.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #ifdef ENABLE_KNEWSTUFF3 #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int CONFIG_MIN_WIDTH = 640; static const int CONFIG_MIN_HEIGHT = 420; } using Tellico::ConfigDialog; ConfigDialog::ConfigDialog(QWidget* parent_) : KPageDialog(parent_) , m_initializedPages(0) , m_modifying(false) , m_okClicked(false) { setFaceType(List); setModal(true); setWindowTitle(i18n("Configure Tellico")); setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); setupGeneralPage(); setupPrintingPage(); setupTemplatePage(); setupFetchPage(); updateGeometry(); QSize s = sizeHint(); resize(qMax(s.width(), CONFIG_MIN_WIDTH), qMax(s.height(), CONFIG_MIN_HEIGHT)); // OK button is connected to buttonBox accepted() signal which is already connected to accept() slot - connect(button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); - connect(button(QDialogButtonBox::Help), SIGNAL(clicked()), SLOT(slotHelp())); - connect(button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked()), SLOT(slotDefault())); + connect(button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ConfigDialog::slotApply); + connect(button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &ConfigDialog::slotHelp); + connect(button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &ConfigDialog::slotDefault); button(QDialogButtonBox::Ok)->setEnabled(false); button(QDialogButtonBox::Apply)->setEnabled(false); button(QDialogButtonBox::Ok)->setDefault(true); button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(this, SIGNAL(currentPageChanged(KPageWidgetItem*, KPageWidgetItem*)), SLOT(slotInitPage(KPageWidgetItem*))); + connect(this, &KPageDialog::currentPageChanged, this, &ConfigDialog::slotInitPage); } ConfigDialog::~ConfigDialog() { foreach(Fetch::ConfigWidget* widget, m_newStuffConfigWidgets) { widget->removed(); } } void ConfigDialog::slotInitPage(KPageWidgetItem* item_) { Q_ASSERT(item_); // every page item has a frame // if the frame has no layout, then we need to initialize the itme QFrame* frame = ::qobject_cast(item_->widget()); Q_ASSERT(frame); if(frame->layout()) { return; } const QString name = item_->name(); // these names must be kept in sync with the page names if(name == i18n("General")) { initGeneralPage(frame); } else if(name == i18n("Printing")) { initPrintingPage(frame); } else if(name == i18n("Templates")) { initTemplatePage(frame); } else if(name == i18n("Data Sources")) { initFetchPage(frame); } } void ConfigDialog::accept() { m_okClicked = true; slotApply(); KPageDialog::accept(); m_okClicked = false; } void ConfigDialog::slotApply() { emit signalConfigChanged(); button(QDialogButtonBox::Apply)->setEnabled(false); } void ConfigDialog::slotDefault() { // only change the defaults on the active page Config::self()->useDefaults(true); const QString name = currentPage()->name(); if(name == i18n("General")) { readGeneralConfig(); } else if(name == i18n("Printing")) { readPrintingConfig(); } else if(name == i18n("Templates")) { readTemplateConfig(); } Config::self()->useDefaults(false); slotModified(); } void ConfigDialog::slotHelp() { const QString name = currentPage()->name(); // these names must be kept in sync with the page names if(name == i18n("General")) { KHelpClient::invokeHelp(QStringLiteral("general-options")); } else if(name == i18n("Printing")) { KHelpClient::invokeHelp(QStringLiteral("printing-options")); } else if(name == i18n("Templates")) { KHelpClient::invokeHelp(QStringLiteral("template-options")); } else if(name == i18n("Data Sources")) { KHelpClient::invokeHelp(QStringLiteral("internet-sources-options")); } } bool ConfigDialog::isPageInitialized(Page page_) const { return m_initializedPages & page_; } void ConfigDialog::setupGeneralPage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("General")); page->setHeader(i18n("General Options")); page->setIcon(QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico")))); addPage(page); // since this is the first page, go ahead and lay it out initGeneralPage(frame); } void ConfigDialog::initGeneralPage(QFrame* frame) { QVBoxLayout* l = new QVBoxLayout(frame); m_cbOpenLastFile = new QCheckBox(i18n("&Reopen file at startup"), frame); m_cbOpenLastFile->setWhatsThis(i18n("If checked, the file that was last open " "will be re-opened at program start-up.")); l->addWidget(m_cbOpenLastFile); - connect(m_cbOpenLastFile, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbOpenLastFile, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); m_cbShowTipDay = new QCheckBox(i18n("&Show \"Tip of the Day\" at startup"), frame); m_cbShowTipDay->setWhatsThis(i18n("If checked, the \"Tip of the Day\" will be " "shown at program start-up.")); l->addWidget(m_cbShowTipDay); - connect(m_cbShowTipDay, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbShowTipDay, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); m_cbEnableWebcam = new QCheckBox(i18n("&Enable webcam for barcode scanning"), frame); m_cbEnableWebcam->setWhatsThis(i18n("If checked, the input from a webcam will be used " "to scan barcodes for searching.")); l->addWidget(m_cbEnableWebcam); - connect(m_cbEnableWebcam, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbEnableWebcam, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); QGroupBox* imageGroupBox = new QGroupBox(i18n("Image Storage Options"), frame); l->addWidget(imageGroupBox); m_rbImageInFile = new QRadioButton(i18n("Store images in data file"), imageGroupBox); m_rbImageInAppDir = new QRadioButton(i18n("Store images in common application directory"), imageGroupBox); m_rbImageInLocalDir = new QRadioButton(i18n("Store images in directory relative to data file"), imageGroupBox); imageGroupBox->setWhatsThis(i18n("Images may be saved in the data file itself, which can " "cause Tellico to run slowly, stored in the Tellico " "application directory, or stored in a directory in the " "same location as the data file.")); QVBoxLayout* imageGroupLayout = new QVBoxLayout(imageGroupBox); imageGroupLayout->addWidget(m_rbImageInFile); imageGroupLayout->addWidget(m_rbImageInAppDir); imageGroupLayout->addWidget(m_rbImageInLocalDir); imageGroupBox->setLayout(imageGroupLayout); QButtonGroup* imageGroup = new QButtonGroup(frame); imageGroup->addButton(m_rbImageInFile); imageGroup->addButton(m_rbImageInAppDir); imageGroup->addButton(m_rbImageInLocalDir); - connect(imageGroup, SIGNAL(buttonClicked(int)), SLOT(slotModified())); + void (QButtonGroup::* buttonClicked)(int) = &QButtonGroup::buttonClicked; + connect(imageGroup, buttonClicked, this, &ConfigDialog::slotModified); QGroupBox* formatGroup = new QGroupBox(i18n("Formatting Options"), frame); l->addWidget(formatGroup); QVBoxLayout* formatGroupLayout = new QVBoxLayout(formatGroup); formatGroup->setLayout(formatGroupLayout); m_cbCapitalize = new QCheckBox(i18n("Auto capitalize &titles and names"), formatGroup); m_cbCapitalize->setWhatsThis(i18n("If checked, titles and names will " "be automatically capitalized.")); - connect(m_cbCapitalize, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbCapitalize, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatGroupLayout->addWidget(m_cbCapitalize); m_cbFormat = new QCheckBox(i18n("Auto &format titles and names"), formatGroup); m_cbFormat->setWhatsThis(i18n("If checked, titles and names will " "be automatically formatted.")); - connect(m_cbFormat, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbFormat, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatGroupLayout->addWidget(m_cbFormat); QWidget* g1 = new QWidget(formatGroup); QGridLayout* g1Layout = new QGridLayout(g1); g1->setLayout(g1Layout); formatGroupLayout->addWidget(g1); QLabel* lab = new QLabel(i18n("No capitali&zation:"), g1); g1Layout->addWidget(lab, 0, 0); m_leCapitals = new QLineEdit(g1); g1Layout->addWidget(m_leCapitals, 0, 1); lab->setBuddy(m_leCapitals); QString whats = i18n("A list of words which should not be capitalized. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_leCapitals->setWhatsThis(whats); - connect(m_leCapitals, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_leCapitals, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("Artic&les:"), g1); g1Layout->addWidget(lab, 1, 0); m_leArticles = new QLineEdit(g1); g1Layout->addWidget(m_leArticles, 1, 1); lab->setBuddy(m_leArticles); whats = i18n("A list of words which should be considered as articles " "if they are the first word in a title. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_leArticles->setWhatsThis(whats); - connect(m_leArticles, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_leArticles, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("Personal suffi&xes:"), g1); g1Layout->addWidget(lab, 2, 0); m_leSuffixes = new QLineEdit(g1); g1Layout->addWidget(m_leSuffixes, 2, 1); lab->setBuddy(m_leSuffixes); whats = i18n("A list of suffixes which might be used in personal names. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_leSuffixes->setWhatsThis(whats); - connect(m_leSuffixes, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_leSuffixes, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("Surname &prefixes:"), g1); g1Layout->addWidget(lab, 3, 0); m_lePrefixes = new QLineEdit(g1); g1Layout->addWidget(m_lePrefixes, 3, 1); lab->setBuddy(m_lePrefixes); whats = i18n("A list of prefixes which might be used in surnames. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_lePrefixes->setWhatsThis(whats); - connect(m_lePrefixes, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_lePrefixes, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); // stretch to fill lower area l->addStretch(1); m_initializedPages |= General; readGeneralConfig(); } void ConfigDialog::setupPrintingPage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("Printing")); page->setHeader(i18n("Printing Options")); page->setIcon(QIcon::fromTheme(QStringLiteral("printer"))); addPage(page); } void ConfigDialog::initPrintingPage(QFrame* frame) { QVBoxLayout* l = new QVBoxLayout(frame); QGroupBox* formatOptions = new QGroupBox(i18n("Formatting Options"), frame); l->addWidget(formatOptions); QVBoxLayout* formatLayout = new QVBoxLayout(formatOptions); formatOptions->setLayout(formatLayout); m_cbPrintFormatted = new QCheckBox(i18n("&Format titles and names"), formatOptions); m_cbPrintFormatted->setWhatsThis(i18n("If checked, titles and names will be automatically formatted.")); - connect(m_cbPrintFormatted, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbPrintFormatted, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatLayout->addWidget(m_cbPrintFormatted); m_cbPrintHeaders = new QCheckBox(i18n("&Print field headers"), formatOptions); m_cbPrintHeaders->setWhatsThis(i18n("If checked, the field names will be printed as table headers.")); - connect(m_cbPrintHeaders, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbPrintHeaders, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatLayout->addWidget(m_cbPrintHeaders); QGroupBox* groupOptions = new QGroupBox(i18n("Grouping Options"), frame); l->addWidget(groupOptions); QVBoxLayout* groupLayout = new QVBoxLayout(groupOptions); groupOptions->setLayout(groupLayout); m_cbPrintGrouped = new QCheckBox(i18n("&Group the entries"), groupOptions); m_cbPrintGrouped->setWhatsThis(i18n("If checked, the entries will be grouped by the selected field.")); - connect(m_cbPrintGrouped, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbPrintGrouped, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); groupLayout->addWidget(m_cbPrintGrouped); QGroupBox* imageOptions = new QGroupBox(i18n("Image Options"), frame); l->addWidget(imageOptions); QGridLayout* gridLayout = new QGridLayout(imageOptions); imageOptions->setLayout(gridLayout); QLabel* lab = new QLabel(i18n("Maximum image &width:"), imageOptions); gridLayout->addWidget(lab, 0, 0); m_imageWidthBox = new QSpinBox(imageOptions); m_imageWidthBox->setMaximum(999); m_imageWidthBox->setMinimum(0); m_imageWidthBox->setValue(50); gridLayout->addWidget(m_imageWidthBox, 0, 1); m_imageWidthBox->setSuffix(QStringLiteral(" px")); lab->setBuddy(m_imageWidthBox); QString whats = i18n("The maximum width of the images in the printout. The aspect ratio is preserved."); lab->setWhatsThis(whats); m_imageWidthBox->setWhatsThis(whats); - connect(m_imageWidthBox, SIGNAL(valueChanged(int)), SLOT(slotModified())); - // QSpinBox doesn't emit valueChanged if you edit the value with - // the lineEdit until you change the keyboard focus -// connect(m_imageWidthBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + void (QSpinBox::* valueChanged)(int) = &QSpinBox::valueChanged; + connect(m_imageWidthBox, valueChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("&Maximum image height:"), imageOptions); gridLayout->addWidget(lab, 1, 0); m_imageHeightBox = new QSpinBox(imageOptions); m_imageHeightBox->setMaximum(999); m_imageHeightBox->setMinimum(0); m_imageHeightBox->setValue(50); gridLayout->addWidget(m_imageHeightBox, 1, 1); m_imageHeightBox->setSuffix(QStringLiteral(" px")); lab->setBuddy(m_imageHeightBox); whats = i18n("The maximum height of the images in the printout. The aspect ratio is preserved."); lab->setWhatsThis(whats); m_imageHeightBox->setWhatsThis(whats); - connect(m_imageHeightBox, SIGNAL(valueChanged(int)), SLOT(slotModified())); - // QSpinBox doesn't emit valueChanged if you edit the value with - // the lineEdit until you change the keyboard focus -// connect(m_imageHeightBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_imageHeightBox, valueChanged, this, &ConfigDialog::slotModified); // stretch to fill lower area l->addStretch(1); m_initializedPages |= Printing; readPrintingConfig(); } void ConfigDialog::setupTemplatePage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("Templates")); page->setHeader(i18n("Template Options")); // odd icon, I know, matches KMail, though... page->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-theme"))); addPage(page); } void ConfigDialog::initTemplatePage(QFrame* frame) { QVBoxLayout* l = new QVBoxLayout(frame); QGridLayout* gridLayout = new QGridLayout(); l->addLayout(gridLayout); int row = -1; // so I can reuse an i18n string, a plain label can't have an '&' QString s = KLocalizedString::removeAcceleratorMarker(i18n("Collection &type:")); QLabel* lab = new QLabel(s, frame); gridLayout->addWidget(lab, ++row, 0); const int collType = Kernel::self()->collectionType(); lab = new QLabel(CollectionFactory::nameHash().value(collType), frame); gridLayout->addWidget(lab, row, 1, 1, 2); lab = new QLabel(i18n("Template:"), frame); m_templateCombo = new GUI::ComboBox(frame); - connect(m_templateCombo, SIGNAL(activated(int)), SLOT(slotModified())); + void (QComboBox::* activatedInt)(int) = &QComboBox::activated; + connect(m_templateCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_templateCombo); QString whats = i18n("Select the template to use for the current type of collections. " "Not all templates will use the font and color settings."); lab->setWhatsThis(whats); m_templateCombo->setWhatsThis(whats); gridLayout->addWidget(lab, ++row, 0); gridLayout->addWidget(m_templateCombo, row, 1); QPushButton* btn = new QPushButton(i18n("&Preview..."), frame); btn->setWhatsThis(i18n("Show a preview of the template")); btn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); gridLayout->addWidget(btn, row, 2); - connect(btn, SIGNAL(clicked()), SLOT(slotShowTemplatePreview())); + connect(btn, &QAbstractButton::clicked, this, &ConfigDialog::slotShowTemplatePreview); // so the button is squeezed small gridLayout->setColumnStretch(0, 10); gridLayout->setColumnStretch(1, 10); loadTemplateList(); // QLabel* l1 = new QLabel(i18n("The options below will be passed to the template, but not " // "all templates will use them. Some fonts and colors may be " // "specified directly in the template."), frame); // l1->setTextFormat(Qt::RichText); // l->addWidget(l1); QGroupBox* fontGroup = new QGroupBox(i18n("Font Options"), frame); l->addWidget(fontGroup); row = -1; QGridLayout* fontLayout = new QGridLayout(); fontGroup->setLayout(fontLayout); lab = new QLabel(i18n("Font:"), fontGroup); fontLayout->addWidget(lab, ++row, 0); m_fontCombo = new QFontComboBox(fontGroup); fontLayout->addWidget(m_fontCombo, row, 1); - connect(m_fontCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_fontCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_fontCombo); whats = i18n("This font is passed to the template used in the Entry View."); lab->setWhatsThis(whats); m_fontCombo->setWhatsThis(whats); fontLayout->addWidget(new QLabel(i18n("Size:"), fontGroup), ++row, 0); m_fontSizeInput = new QSpinBox(fontGroup); m_fontSizeInput->setMaximum(30); // 30 is same max as konq config m_fontSizeInput->setMinimum(5); m_fontSizeInput->setSuffix(QStringLiteral("pt")); fontLayout->addWidget(m_fontSizeInput, row, 1); - connect(m_fontSizeInput, SIGNAL(valueChanged(int)), SLOT(slotModified())); + void (QSpinBox::* valueChangedInt)(int) = &QSpinBox::valueChanged; + connect(m_fontSizeInput, valueChangedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_fontSizeInput); lab->setWhatsThis(whats); m_fontSizeInput->setWhatsThis(whats); QGroupBox* colGroup = new QGroupBox(i18n("Color Options"), frame); l->addWidget(colGroup); row = -1; QGridLayout* colLayout = new QGridLayout(); colGroup->setLayout(colLayout); lab = new QLabel(i18n("Background color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_baseColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_baseColorCombo, row, 1); - connect(m_baseColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_baseColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_baseColorCombo); whats = i18n("This color is passed to the template used in the Entry View."); lab->setWhatsThis(whats); m_baseColorCombo->setWhatsThis(whats); lab = new QLabel(i18n("Text color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_textColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_textColorCombo, row, 1); - connect(m_textColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_textColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_textColorCombo); lab->setWhatsThis(whats); m_textColorCombo->setWhatsThis(whats); lab = new QLabel(i18n("Highlight color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_highBaseColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_highBaseColorCombo, row, 1); - connect(m_highBaseColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_highBaseColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_highBaseColorCombo); lab->setWhatsThis(whats); m_highBaseColorCombo->setWhatsThis(whats); lab = new QLabel(i18n("Highlighted text color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_highTextColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_highTextColorCombo, row, 1); - connect(m_highTextColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_highTextColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_highTextColorCombo); lab->setWhatsThis(whats); m_highTextColorCombo->setWhatsThis(whats); QGroupBox* groupBox = new QGroupBox(i18n("Manage Templates"), frame); l->addWidget(groupBox); QVBoxLayout* vlay = new QVBoxLayout(groupBox); groupBox->setLayout(vlay); QWidget* box1 = new QWidget(groupBox); QHBoxLayout* box1HBoxLayout = new QHBoxLayout(box1); box1HBoxLayout->setMargin(0); vlay->addWidget(box1); box1HBoxLayout->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPushButton* b1 = new QPushButton(i18n("Install..."), box1); box1HBoxLayout->addWidget(b1); b1->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); - connect(b1, SIGNAL(clicked()), SLOT(slotInstallTemplate())); + connect(b1, &QAbstractButton::clicked, this, &ConfigDialog::slotInstallTemplate); whats = i18n("Click to install a new template directly."); b1->setWhatsThis(whats); QPushButton* b2 = new QPushButton(i18n("Download..."), box1); box1HBoxLayout->addWidget(b2); b2->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); - connect(b2, SIGNAL(clicked()), SLOT(slotDownloadTemplate())); + connect(b2, &QAbstractButton::clicked, this, &ConfigDialog::slotDownloadTemplate); whats = i18n("Click to download additional templates."); b2->setWhatsThis(whats); QPushButton* b3 = new QPushButton(i18n("Delete..."), box1); box1HBoxLayout->addWidget(b3); b3->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); - connect(b3, SIGNAL(clicked()), SLOT(slotDeleteTemplate())); + connect(b3, &QAbstractButton::clicked, this, &ConfigDialog::slotDeleteTemplate); whats = i18n("Click to select and remove installed templates."); b3->setWhatsThis(whats); // stretch to fill lower area l->addStretch(1); // purely for aesthetics make all widgets line up QList widgets; widgets.append(m_fontCombo); widgets.append(m_fontSizeInput); widgets.append(m_baseColorCombo); widgets.append(m_textColorCombo); widgets.append(m_highBaseColorCombo); widgets.append(m_highTextColorCombo); int w = 0; foreach(QWidget* widget, widgets) { widget->ensurePolished(); w = qMax(w, widget->sizeHint().width()); } foreach(QWidget* widget, widgets) { widget->setMinimumWidth(w); } KAcceleratorManager::manage(frame); m_initializedPages |= Template; readTemplateConfig(); } void ConfigDialog::setupFetchPage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("Data Sources")); page->setHeader(i18n("Data Sources Options")); page->setIcon(QIcon::fromTheme(QStringLiteral("network-wired"))); addPage(page); } void ConfigDialog::initFetchPage(QFrame* frame) { QHBoxLayout* l = new QHBoxLayout(frame); QVBoxLayout* leftLayout = new QVBoxLayout(); l->addLayout(leftLayout); m_sourceListWidget = new QListWidget(frame); m_sourceListWidget->setSortingEnabled(false); // no sorting m_sourceListWidget->setSelectionMode(QAbstractItemView::SingleSelection); leftLayout->addWidget(m_sourceListWidget, 1); - connect(m_sourceListWidget, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), SLOT(slotSelectedSourceChanged(QListWidgetItem*))); - connect(m_sourceListWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)), SLOT(slotModifySourceClicked())); + connect(m_sourceListWidget, &QListWidget::currentItemChanged, this, &ConfigDialog::slotSelectedSourceChanged); + connect(m_sourceListWidget, &QListWidget::itemDoubleClicked, this, &ConfigDialog::slotModifySourceClicked); QWidget* hb = new QWidget(frame); QHBoxLayout* hbHBoxLayout = new QHBoxLayout(hb); hbHBoxLayout->setMargin(0); leftLayout->addWidget(hb); m_moveUpSourceBtn = new QPushButton(i18n("Move &Up"), hb); hbHBoxLayout->addWidget(m_moveUpSourceBtn); m_moveUpSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_moveUpSourceBtn->setWhatsThis(i18n("The order of the data sources sets the order " "that Tellico uses when entries are automatically updated.")); m_moveDownSourceBtn = new QPushButton(i18n("Move &Down"), hb); hbHBoxLayout->addWidget(m_moveDownSourceBtn); m_moveDownSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_moveDownSourceBtn->setWhatsThis(i18n("The order of the data sources sets the order " "that Tellico uses when entries are automatically updated.")); QWidget* hb2 = new QWidget(frame); QHBoxLayout* hb2HBoxLayout = new QHBoxLayout(hb2); hb2HBoxLayout->setMargin(0); leftLayout->addWidget(hb2); m_cbFilterSource = new QCheckBox(i18n("Filter by type:"), hb2); hb2HBoxLayout->addWidget(m_cbFilterSource); - connect(m_cbFilterSource, SIGNAL(clicked()), SLOT(slotSourceFilterChanged())); + connect(m_cbFilterSource, &QAbstractButton::clicked, this, &ConfigDialog::slotSourceFilterChanged); m_sourceTypeCombo = new GUI::CollectionTypeCombo(hb2); hb2HBoxLayout->addWidget(m_sourceTypeCombo); - connect(m_sourceTypeCombo, SIGNAL(currentIndexChanged(int)), SLOT(slotSourceFilterChanged())); + void (QComboBox::* currentIndexChanged)(int) = &QComboBox::currentIndexChanged; + connect(m_sourceTypeCombo, currentIndexChanged, this, &ConfigDialog::slotSourceFilterChanged); // we want to remove the item for a custom collection int index = m_sourceTypeCombo->findData(Data::Collection::Base); if(index > -1) { m_sourceTypeCombo->removeItem(index); } // disable until check box is checked m_sourceTypeCombo->setEnabled(false); // these icons are rather arbitrary, but seem to vaguely fit QVBoxLayout* vlay = new QVBoxLayout(); l->addLayout(vlay); QPushButton* newSourceBtn = new QPushButton(i18n("&New..."), frame); newSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); newSourceBtn->setWhatsThis(i18n("Click to add a new data source.")); m_modifySourceBtn = new QPushButton(i18n("&Modify..."), frame); m_modifySourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("network-wired"))); m_modifySourceBtn->setWhatsThis(i18n("Click to modify the selected data source.")); m_removeSourceBtn = new QPushButton(i18n("&Delete"), frame); m_removeSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_removeSourceBtn->setWhatsThis(i18n("Click to delete the selected data source.")); m_newStuffBtn = new QPushButton(i18n("Download..."), frame); m_newStuffBtn->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); m_newStuffBtn->setWhatsThis(i18n("Click to download additional data sources.")); // checksum and signature checking are no longer possible with knewstuff2 // disable button for now m_newStuffBtn->setEnabled(false); vlay->addWidget(newSourceBtn); vlay->addWidget(m_modifySourceBtn); vlay->addWidget(m_removeSourceBtn); // separate newstuff button from the rest vlay->addSpacing(16); vlay->addWidget(m_newStuffBtn); vlay->addStretch(1); - connect(newSourceBtn, SIGNAL(clicked()), SLOT(slotNewSourceClicked())); - connect(m_modifySourceBtn, SIGNAL(clicked()), SLOT(slotModifySourceClicked())); - connect(m_moveUpSourceBtn, SIGNAL(clicked()), SLOT(slotMoveUpSourceClicked())); - connect(m_moveDownSourceBtn, SIGNAL(clicked()), SLOT(slotMoveDownSourceClicked())); - connect(m_removeSourceBtn, SIGNAL(clicked()), SLOT(slotRemoveSourceClicked())); - connect(m_newStuffBtn, SIGNAL(clicked()), SLOT(slotNewStuffClicked())); + connect(newSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotNewSourceClicked); + connect(m_modifySourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotModifySourceClicked); + connect(m_moveUpSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotMoveUpSourceClicked); + connect(m_moveDownSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotMoveDownSourceClicked); + connect(m_removeSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotRemoveSourceClicked); + connect(m_newStuffBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotNewStuffClicked); KAcceleratorManager::manage(frame); m_initializedPages |= Fetch; readFetchConfig(); } void ConfigDialog::readGeneralConfig() { m_modifying = true; m_cbShowTipDay->setChecked(Config::showTipOfDay()); m_cbOpenLastFile->setChecked(Config::reopenLastFile()); #ifdef ENABLE_WEBCAM m_cbEnableWebcam->setChecked(Config::enableWebcam()); #else m_cbEnableWebcam->setChecked(false); m_cbEnableWebcam->setEnabled(false); #endif switch(Config::imageLocation()) { case Config::ImagesInFile: m_rbImageInFile->setChecked(true); break; case Config::ImagesInAppDir: m_rbImageInAppDir->setChecked(true); break; case Config::ImagesInLocalDir: m_rbImageInLocalDir->setChecked(true); break; } bool autoCapitals = Config::autoCapitalization(); m_cbCapitalize->setChecked(autoCapitals); bool autoFormat = Config::autoFormat(); m_cbFormat->setChecked(autoFormat); const QRegExp comma(QLatin1String("\\s*,\\s*")); m_leCapitals->setText(Config::noCapitalizationString().replace(comma, FieldFormat::delimiterString())); m_leArticles->setText(Config::articlesString().replace(comma, FieldFormat::delimiterString())); m_leSuffixes->setText(Config::nameSuffixesString().replace(comma, FieldFormat::delimiterString())); m_lePrefixes->setText(Config::surnamePrefixesString().replace(comma, FieldFormat::delimiterString())); m_modifying = false; } void ConfigDialog::readPrintingConfig() { m_modifying = true; m_cbPrintHeaders->setChecked(Config::printFieldHeaders()); m_cbPrintFormatted->setChecked(Config::printFormatted()); m_cbPrintGrouped->setChecked(Config::printGrouped()); m_imageWidthBox->setValue(Config::maxImageWidth()); m_imageHeightBox->setValue(Config::maxImageHeight()); m_modifying = false; } void ConfigDialog::readTemplateConfig() { m_modifying = true; // entry template selection const int collType = Kernel::self()->collectionType(); QString file = Config::templateName(collType); file.replace(QLatin1Char('_'), QLatin1Char(' ')); QString fileContext = file + QLatin1String(" XSL Template"); m_templateCombo->setCurrentItem(i18nc(fileContext.toUtf8().constData(), file.toUtf8().constData())); m_fontCombo->setCurrentFont(QFont(Config::templateFont(collType).family())); m_fontSizeInput->setValue(Config::templateFont(collType).pointSize()); m_baseColorCombo->setColor(Config::templateBaseColor(collType)); m_textColorCombo->setColor(Config::templateTextColor(collType)); m_highBaseColorCombo->setColor(Config::templateHighlightedBaseColor(collType)); m_highTextColorCombo->setColor(Config::templateHighlightedTextColor(collType)); m_modifying = false; } void ConfigDialog::readFetchConfig() { m_modifying = true; m_sourceListWidget->clear(); m_configWidgets.clear(); m_sourceListWidget->setUpdatesEnabled(false); foreach(Fetch::Fetcher::Ptr fetcher, Fetch::Manager::self()->fetchers()) { Fetch::FetcherInfo info(fetcher->type(), fetcher->source(), fetcher->updateOverwrite(), fetcher->uuid()); FetcherInfoListItem* item = new FetcherInfoListItem(m_sourceListWidget, info); item->setFetcher(fetcher); } m_sourceListWidget->setUpdatesEnabled(true); if(m_sourceListWidget->count() == 0) { m_modifySourceBtn->setEnabled(false); m_removeSourceBtn->setEnabled(false); } else { // go ahead and select the first one m_sourceListWidget->setCurrentItem(m_sourceListWidget->item(0)); } m_modifying = false; - QTimer::singleShot(500, this, SLOT(slotCreateConfigWidgets())); + QTimer::singleShot(500, this, &ConfigDialog::slotCreateConfigWidgets); } void ConfigDialog::saveConfiguration() { if(isPageInitialized(General)) saveGeneralConfig(); if(isPageInitialized(Printing)) savePrintingConfig(); if(isPageInitialized(Template)) saveTemplateConfig(); if(isPageInitialized(Fetch)) saveFetchConfig(); } void ConfigDialog::saveGeneralConfig() { Config::setShowTipOfDay(m_cbShowTipDay->isChecked()); Config::setEnableWebcam(m_cbEnableWebcam->isChecked()); int imageLocation; if(m_rbImageInFile->isChecked()) { imageLocation = Config::ImagesInFile; } else if(m_rbImageInAppDir->isChecked()) { imageLocation = Config::ImagesInAppDir; } else { imageLocation = Config::ImagesInLocalDir; } Config::setImageLocation(imageLocation); Config::setReopenLastFile(m_cbOpenLastFile->isChecked()); Config::setAutoCapitalization(m_cbCapitalize->isChecked()); Config::setAutoFormat(m_cbFormat->isChecked()); const QRegExp semicolon(QLatin1String("\\s*;\\s*")); const QChar comma = QLatin1Char(','); Config::setNoCapitalizationString(m_leCapitals->text().replace(semicolon, comma)); Config::setArticlesString(m_leArticles->text().replace(semicolon, comma)); Config::setNameSuffixesString(m_leSuffixes->text().replace(semicolon, comma)); Config::setSurnamePrefixesString(m_lePrefixes->text().replace(semicolon, comma)); } void ConfigDialog::savePrintingConfig() { Config::setPrintFieldHeaders(m_cbPrintHeaders->isChecked()); Config::setPrintFormatted(m_cbPrintFormatted->isChecked()); Config::setPrintGrouped(m_cbPrintGrouped->isChecked()); Config::setMaxImageWidth(m_imageWidthBox->value()); Config::setMaxImageHeight(m_imageHeightBox->value()); } void ConfigDialog::saveTemplateConfig() { const int collType = Kernel::self()->collectionType(); Config::setTemplateName(collType, m_templateCombo->currentData().toString()); QFont font(m_fontCombo->currentFont().family(), m_fontSizeInput->value()); Config::setTemplateFont(collType, font); Config::setTemplateBaseColor(collType, m_baseColorCombo->color()); Config::setTemplateTextColor(collType, m_textColorCombo->color()); Config::setTemplateHighlightedBaseColor(collType, m_highBaseColorCombo->color()); Config::setTemplateHighlightedTextColor(collType, m_highTextColorCombo->color()); } void ConfigDialog::saveFetchConfig() { // first, tell config widgets they got deleted foreach(Fetch::ConfigWidget* widget, m_removedConfigWidgets) { widget->removed(); } m_removedConfigWidgets.clear(); bool reloadFetchers = false; int count = 0; // start group numbering at 0 for( ; count < m_sourceListWidget->count(); ++count) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(count)); Fetch::ConfigWidget* cw = m_configWidgets[item]; if(!cw || (!cw->shouldSave() && !item->isNewSource())) { continue; } m_newStuffConfigWidgets.removeAll(cw); QString group = QStringLiteral("Data Source %1").arg(count); // in case we later change the order, clear the group now KSharedConfig::openConfig()->deleteGroup(group); KConfigGroup configGroup(KSharedConfig::openConfig(), group); configGroup.writeEntry("Name", item->data(Qt::DisplayRole).toString()); configGroup.writeEntry("Type", int(item->fetchType())); configGroup.writeEntry("UpdateOverwrite", item->updateOverwrite()); configGroup.writeEntry("Uuid", item->uuid()); cw->saveConfig(configGroup); item->setNewSource(false); // in case the ordering changed item->setConfigGroup(group); reloadFetchers = true; } // now update total number of sources KConfigGroup sourceGroup(KSharedConfig::openConfig(), "Data Sources"); sourceGroup.writeEntry("Sources Count", count); // and purge old config groups QString group = QStringLiteral("Data Source %1").arg(count); while(KSharedConfig::openConfig()->hasGroup(group)) { KSharedConfig::openConfig()->deleteGroup(group); ++count; group = QStringLiteral("Data Source %1").arg(count); } Config::self()->save(); if(reloadFetchers) { Fetch::Manager::self()->loadFetchers(); Controller::self()->updatedFetchers(); // reload fetcher items if OK was not clicked // meaning apply was clicked if(!m_okClicked) { QString currentSource; if(m_sourceListWidget->currentItem()) { currentSource = m_sourceListWidget->currentItem()->data(Qt::DisplayRole).toString(); } readFetchConfig(); if(!currentSource.isEmpty()) { QList items = m_sourceListWidget->findItems(currentSource, Qt::MatchExactly); if(!items.isEmpty()) { m_sourceListWidget->setCurrentItem(items.first()); m_sourceListWidget->scrollToItem(items.first()); } } } } } void ConfigDialog::slotModified() { if(m_modifying) { return; } button(QDialogButtonBox::Ok)->setEnabled(true); button(QDialogButtonBox::Apply)->setEnabled(true); } void ConfigDialog::slotNewSourceClicked() { FetcherConfigDialog dlg(this); if(dlg.exec() != QDialog::Accepted) { return; } Fetch::Type type = dlg.sourceType(); if(type == Fetch::Unknown) { return; } Fetch::FetcherInfo info(type, dlg.sourceName(), dlg.updateOverwrite()); FetcherInfoListItem* item = new FetcherInfoListItem(m_sourceListWidget, info); m_sourceListWidget->scrollToItem(item); m_sourceListWidget->setCurrentItem(item); Fetch::ConfigWidget* cw = dlg.configWidget(); if(cw) { cw->setAccepted(true); cw->slotSetModified(); cw->setParent(this); // keep the config widget around m_configWidgets.insert(item, cw); } m_modifySourceBtn->setEnabled(true); m_removeSourceBtn->setEnabled(true); slotModified(); // toggle apply button } void ConfigDialog::slotModifySourceClicked() { FetcherInfoListItem* item = static_cast(m_sourceListWidget->currentItem()); if(!item) { return; } Fetch::ConfigWidget* cw = nullptr; if(m_configWidgets.contains(item)) { cw = m_configWidgets[item]; } else { // grab the config widget, taking ownership cw = item->fetcher()->configWidget(this); if(cw) { // might return 0 when no widget available for fetcher type m_configWidgets.insert(item, cw); // there's weird layout bug if it's not hidden cw->hide(); } } if(!cw) { // no config widget for this one // might be because support was compiled out myDebug() << "no config widget for source" << item->data(Qt::DisplayRole).toString(); return; } FetcherConfigDialog dlg(item->data(Qt::DisplayRole).toString(), item->fetchType(), item->updateOverwrite(), cw, this); if(dlg.exec() == QDialog::Accepted) { cw->setAccepted(true); // mark to save QString newName = dlg.sourceName(); if(newName != item->data(Qt::DisplayRole).toString()) { item->setData(Qt::DisplayRole, newName); cw->slotSetModified(); } item->setUpdateOverwrite(dlg.updateOverwrite()); slotModified(); // toggle apply button } cw->setParent(this); // keep the config widget around } void ConfigDialog::slotRemoveSourceClicked() { FetcherInfoListItem* item = static_cast(m_sourceListWidget->currentItem()); if(!item) { return; } Tellico::NewStuff::Manager::self()->removeScriptByName(item->text()); Fetch::ConfigWidget* cw = m_configWidgets[item]; if(cw) { m_removedConfigWidgets.append(cw); // it gets deleted by the parent } m_configWidgets.remove(item); delete item; // m_sourceListWidget->setCurrentItem(m_sourceListWidget->currentItem()); slotModified(); // toggle apply button } void ConfigDialog::slotMoveUpSourceClicked() { int row = m_sourceListWidget->currentRow(); if(row < 1) { return; } QListWidgetItem* item = m_sourceListWidget->takeItem(row); m_sourceListWidget->insertItem(row-1, item); m_sourceListWidget->setCurrentItem(item); slotModified(); // toggle apply button } void ConfigDialog::slotMoveDownSourceClicked() { int row = m_sourceListWidget->currentRow(); if(row > m_sourceListWidget->count()-2) { return; } QListWidgetItem* item = m_sourceListWidget->takeItem(row); m_sourceListWidget->insertItem(row+1, item); m_sourceListWidget->setCurrentItem(item); slotModified(); // toggle apply button } void ConfigDialog::slotSourceFilterChanged() { m_sourceTypeCombo->setEnabled(m_cbFilterSource->isChecked()); const bool showAll = !m_sourceTypeCombo->isEnabled(); const int type = m_sourceTypeCombo->currentType(); for(int count = 0; count < m_sourceListWidget->count(); ++count) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(count)); item->setHidden(!showAll && item->fetcher() && !item->fetcher()->canFetch(type)); } } void ConfigDialog::slotSelectedSourceChanged(QListWidgetItem* item_) { int row = m_sourceListWidget->row(item_); m_moveUpSourceBtn->setEnabled(row > 0); m_moveDownSourceBtn->setEnabled(row < m_sourceListWidget->count()-1); } void ConfigDialog::slotNewStuffClicked() { #ifdef ENABLE_KNEWSTUFF3 KNS3::DownloadDialog dialog(QStringLiteral("tellico-script.knsrc"), this); dialog.exec(); KNS3::Entry::List entries = dialog.installedEntries(); if(!entries.isEmpty()) { Fetch::Manager::self()->loadFetchers(); readFetchConfig(); } #endif } Tellico::FetcherInfoListItem* ConfigDialog::findItem(const QString& path_) const { if(path_.isEmpty()) { myDebug() << "empty path"; return nullptr; } // this is a bit ugly, loop over all items, find the execexternal one // that matches the path for(int i = 0; i < m_sourceListWidget->count(); ++i) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(i)); if(item->fetchType() != Fetch::ExecExternal) { continue; } Fetch::ExecExternalFetcher* f = dynamic_cast(item->fetcher().data()); if(f && f->execPath() == path_) { return item; } } myDebug() << "no matching item found"; return nullptr; } void ConfigDialog::slotShowTemplatePreview() { GUI::PreviewDialog* dlg = new GUI::PreviewDialog(this); const QString templateName = m_templateCombo->currentData().toString(); dlg->setXSLTFile(templateName + QLatin1String(".xsl")); StyleOptions options; options.fontFamily = m_fontCombo->currentFont().family(); options.fontSize = m_fontSizeInput->value(); options.baseColor = m_baseColorCombo->color(); options.textColor = m_textColorCombo->color(); options.highlightedTextColor = m_highTextColorCombo->color(); options.highlightedBaseColor = m_highBaseColorCombo->color(); dlg->setXSLTOptions(Kernel::self()->collectionType(), options); Data::CollPtr c = CollectionFactory::collection(Kernel::self()->collectionType(), true); Data::EntryPtr e(new Data::Entry(c)); foreach(Data::FieldPtr f, c->fields()) { if(f->name() == QLatin1String("title")) { e->setField(f->name(), m_templateCombo->currentText()); } else if(f->type() == Data::Field::Image) { continue; } else if(f->type() == Data::Field::Choice) { e->setField(f->name(), f->allowed().front()); } else if(f->type() == Data::Field::Number) { e->setField(f->name(), QStringLiteral("1")); } else if(f->type() == Data::Field::Bool) { e->setField(f->name(), QStringLiteral("true")); } else if(f->type() == Data::Field::Rating) { e->setField(f->name(), QStringLiteral("5")); } else { e->setField(f->name(), f->title()); } } dlg->showEntry(e); dlg->show(); // dlg gets deleted by itself // the finished() signal is connected in its constructor to delayedDestruct } void ConfigDialog::loadTemplateList() { QStringList files = Tellico::locateAllFiles(QStringLiteral("tellico/entry-templates/*.xsl")); QMap templates; // a QMap will have them values sorted by key foreach(const QString& file, files) { QFileInfo fi(file); QString lfile = fi.fileName().section(QLatin1Char('.'), 0, -2); QString name = lfile; name.replace(QLatin1Char('_'), QLatin1Char(' ')); QString title = i18nc((name + QLatin1String(" XSL Template")).toUtf8().constData(), name.toUtf8().constData()); templates.insert(title, lfile); } QString s = m_templateCombo->currentText(); m_templateCombo->clear(); for(QMap::ConstIterator it2 = templates.constBegin(); it2 != templates.constEnd(); ++it2) { m_templateCombo->addItem(it2.key(), it2.value()); } m_templateCombo->setCurrentItem(s); } void ConfigDialog::slotInstallTemplate() { QString filter = i18n("XSL Files") + QLatin1String(" (*.xsl)") + QLatin1String(";;"); filter += i18n("Template Packages") + QLatin1String(" (*.tar.gz *.tgz)") + QLatin1String(";;"); filter += i18n("All Files") + QLatin1String(" (*)"); const QString fileClass(QStringLiteral(":InstallTemplate")); const QString f = QFileDialog::getOpenFileName(this, QString(), KRecentDirs::dir(fileClass), filter); if(f.isEmpty()) { return; } KRecentDirs::add(fileClass, QFileInfo(f).dir().canonicalPath()); if(Tellico::NewStuff::Manager::self()->installTemplate(f)) { loadTemplateList(); } } void ConfigDialog::slotDownloadTemplate() { #ifdef ENABLE_KNEWSTUFF3 KNS3::DownloadDialog dialog(QStringLiteral("tellico-template.knsrc"), this); dialog.exec(); KNS3::Entry::List entries = dialog.installedEntries(); if(!entries.isEmpty()) { loadTemplateList(); } #endif } void ConfigDialog::slotDeleteTemplate() { bool ok; QString name = QInputDialog::getItem(this, i18n("Delete Template"), i18n("Select template to delete:"), Tellico::NewStuff::Manager::self()->userTemplates().keys(), 0, false, &ok); if(ok && !name.isEmpty()) { Tellico::NewStuff::Manager::self()->removeTemplateByName(name); loadTemplateList(); } } void ConfigDialog::slotCreateConfigWidgets() { for(int count = 0; count < m_sourceListWidget->count(); ++count) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(count)); // only create a new config widget if we don't have one already if(!m_configWidgets.contains(item)) { Fetch::ConfigWidget* cw = item->fetcher()->configWidget(this); if(cw) { // might return 0 when no widget available for fetcher type m_configWidgets.insert(item, cw); // there's weird layout bug if it's not hidden cw->hide(); } } } } diff --git a/src/detailedlistview.cpp b/src/detailedlistview.cpp index 22c66bac..5f1bb106 100644 --- a/src/detailedlistview.cpp +++ b/src/detailedlistview.cpp @@ -1,645 +1,639 @@ /*************************************************************************** Copyright (C) 2001-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 . * * * ***************************************************************************/ #include "detailedlistview.h" #include "collection.h" #include "collectionfactory.h" #include "controller.h" #include "field.h" #include "entry.h" #include "tellico_debug.h" #include "tellico_kernel.h" #include "config/tellico_config.h" #include "models/entrymodel.h" #include "models/entrysortmodel.h" #include "models/modelmanager.h" #include "gui/detailedentryitemdelegate.h" #include "gui/ratingdelegate.h" #include "utils/string_utils.h" #include #include #include #include #include #include using namespace Tellico; using Tellico::DetailedListView; DetailedListView::DetailedListView(QWidget* parent_) : GUI::TreeView(parent_) - , m_loadingCollection(false), m_selectionChanging(false), m_currentContextColumn(-1) { + , m_loadingCollection(false), m_currentContextColumn(-1) { setHeaderHidden(false); setSelectionMode(QAbstractItemView::ExtendedSelection); setAlternatingRowColors(true); setRootIsDecorated(false); setUniformRowHeights(true); - connect(this, SIGNAL(doubleClicked(const QModelIndex&)), SLOT(slotDoubleClicked(const QModelIndex&))); + connect(this, &QAbstractItemView::doubleClicked, this, &DetailedListView::slotDoubleClicked); // header menu header()->installEventFilter(this); header()->setMinimumSectionSize(20); m_headerMenu = new QMenu(this); m_columnMenu = new QMenu(this); - connect(m_columnMenu, SIGNAL(triggered(QAction*)), - SLOT(slotColumnMenuActivated(QAction*))); + connect(m_columnMenu, &QMenu::triggered, + this, &DetailedListView::slotColumnMenuActivated); EntryModel* entryModel = new EntryModel(this); EntrySortModel* sortModel = new EntrySortModel(this); sortModel->setSortRole(EntryPtrRole); sortModel->setSourceModel(entryModel); setModel(sortModel); setItemDelegate(new DetailedEntryItemDelegate(this)); ModelManager::self()->setEntryModel(sortModel); - connect(model(), SIGNAL(headerDataChanged(Qt::Orientation, int, int)), SLOT(updateHeaderMenu())); - connect(model(), SIGNAL(headerDataChanged(Qt::Orientation, int, int)), SLOT(updateColumnDelegates())); - connect(model(), SIGNAL(columnsInserted(const QModelIndex&, int, int)), SLOT(hideNewColumn(const QModelIndex&, int, int))); - connect(header(), SIGNAL(sectionCountChanged(int, int)), SLOT(updateHeaderMenu())); + connect(model(), &QAbstractItemModel::headerDataChanged, this, &DetailedListView::updateHeaderMenu); + connect(model(), &QAbstractItemModel::headerDataChanged, this, &DetailedListView::updateColumnDelegates); + connect(model(), &QAbstractItemModel::columnsInserted, this, &DetailedListView::hideNewColumn); + connect(header(), &QHeaderView::sectionCountChanged, this, &DetailedListView::updateHeaderMenu); } DetailedListView::~DetailedListView() { } Tellico::EntryModel* DetailedListView::sourceModel() const { return static_cast(sortModel()->sourceModel()); } void DetailedListView::addCollection(Tellico::Data::CollPtr coll_) { if(!coll_) { return; } const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_)); KConfigGroup config(KSharedConfig::openConfig(), configGroup); QString configN; if(coll_->type() == Data::Collection::Base) { QUrl url = Kernel::self()->URL(); for(int i = 0; i < Config::maxCustomURLSettings(); ++i) { QUrl u = config.readEntry(QStringLiteral("URL_%1").arg(i), QUrl()); if(u == url) { configN = QStringLiteral("_%1").arg(i); break; } } } // we don't want to immediately hide all these columns when adding fields - disconnect(model(), SIGNAL(columnsInserted(const QModelIndex&, int, int)), - this, SLOT(hideNewColumn(const QModelIndex&, int, int))); + disconnect(model(), &QAbstractItemModel::columnsInserted, + this, &DetailedListView::hideNewColumn); sourceModel()->setImagesAreAvailable(false); sourceModel()->setFields(coll_->fields()); - connect(model(), SIGNAL(columnsInserted(const QModelIndex&, int, int)), - this, SLOT(hideNewColumn(const QModelIndex&, int, int))); + connect(model(), &QAbstractItemModel::columnsInserted, + this, &DetailedListView::hideNewColumn); // we're not using saveState() and restoreState() since our columns are variable QStringList columnNames = config.readEntry(QLatin1String("ColumnNames") + configN, QStringList()); QList columnWidths = config.readEntry(QLatin1String("ColumnWidths") + configN, QList()); QList columnOrder = config.readEntry(QLatin1String("ColumnOrder") + configN, QList()); // just a broken-world check while(columnWidths.size() < columnNames.size()) { columnWidths << 0; } while(columnOrder.size() < columnNames.size()) { columnOrder << columnOrder.size(); } QList currentColumnOrder; // now restore widths and order for(int ncol = 0; ncol < header()->count(); ++ncol) { int idx = columnNames.indexOf(columnFieldName(ncol)); // column width of 0 means hidden if(idx < 0 || columnWidths.at(idx) <= 0) { hideColumn(ncol); if(idx > -1) { currentColumnOrder << ncol; } } else { setColumnWidth(ncol, columnWidths.at(idx)); currentColumnOrder << ncol; } } const int maxCount = qMin(currentColumnOrder.size(), columnOrder.size()); for(int i = 0; i < maxCount; ++i) { header()->moveSection(header()->visualIndex(currentColumnOrder.at(i)), columnOrder.at(i)); } // always hide tables and paragraphs for(int ncol = 0; ncol < coll_->fields().count(); ++ncol) { Data::FieldPtr field = model()->headerData(ncol, Qt::Horizontal, FieldPtrRole).value(); if(field) { if(field->type() == Data::Field::Table || field->type() == Data::Field::Para) { hideColumn(ncol); } } else { myDebug() << "no field for col" << ncol; } } // because some of the fields got hidden... updateColumnDelegates(); updateHeaderMenu(); checkHeader(); sortModel()->setSortColumn(config.readEntry(QLatin1String("SortColumn") + configN, -1)); sortModel()->setSecondarySortColumn(config.readEntry(QLatin1String("PrevSortColumn") + configN, -1)); sortModel()->setTertiarySortColumn(config.readEntry(QLatin1String("Prev2SortColumn") + configN, -1)); const int order = config.readEntry(QLatin1String("SortOrder") + configN, static_cast(Qt::AscendingOrder)); sortModel()->setSortOrder(static_cast(order)); setUpdatesEnabled(false); m_loadingCollection = true; addEntries(coll_->entries()); m_loadingCollection = false; setUpdatesEnabled(true); header()->setSortIndicator(sortModel()->sortColumn(), sortModel()->sortOrder()); } void DetailedListView::slotReset() { //clear() does not remove columns sourceModel()->clear(); } void DetailedListView::addEntries(Tellico::Data::EntryList entries_) { if(entries_.isEmpty()) { return; } sourceModel()->addEntries(entries_); if(!m_loadingCollection) { setState(entries_, NewState); - if(!m_selectionChanging) { - setEntriesSelected(entries_); - } } } void DetailedListView::modifyEntries(Tellico::Data::EntryList entries_) { if(entries_.isEmpty()) { return; } sourceModel()->modifyEntries(entries_); setState(entries_, ModifiedState); - if(!m_selectionChanging) { - setEntriesSelected(entries_); - } } void DetailedListView::removeEntries(Tellico::Data::EntryList entries_) { if(entries_.isEmpty()) { return; } sourceModel()->removeEntries(entries_); } void DetailedListView::setState(Tellico::Data::EntryList entries_, int state) { foreach(Data::EntryPtr entry, entries_) { QModelIndex index = sourceModel()->indexFromEntry(entry); if(index.isValid()) { sourceModel()->setData(index, state, SaveStateRole); } else { myWarning() << "no index found for" << entry->id() << entry->title(); } } } void DetailedListView::removeCollection(Tellico::Data::CollPtr coll_) { if(!coll_) { myWarning() << "null coll pointer!"; return; } sourceModel()->clear(); } void DetailedListView::contextMenuEvent(QContextMenuEvent* event_) { QModelIndex index = indexAt(event_->pos()); if(!index.isValid()) { return; } QMenu menu(this); Controller::self()->plugEntryActions(&menu); menu.exec(event_->globalPos()); } // don't shadow QListView::setSelected void DetailedListView::setEntriesSelected(Data::EntryList entries_) { if(entries_.isEmpty()) { // don't move this one outside the block since it calls setCurrentItem(0) clearSelection(); return; } clearSelection(); EntrySortModel* proxyModel = static_cast(model()); foreach(Data::EntryPtr entry, entries_) { QModelIndex index = sourceModel()->indexFromEntry(entry); if(!proxyModel->mapFromSource(index).isValid()) { // clear the filter if we're trying to select an entry that is currently filtered out Controller::self()->clearFilter(); break; } } blockSignals(true); foreach(Data::EntryPtr entry, entries_) { QModelIndex index = sourceModel()->indexFromEntry(entry); selectionModel()->select(proxyModel->mapFromSource(index), QItemSelectionModel::Select | QItemSelectionModel::Rows); } //setCurrentIndex(index); blockSignals(false); QModelIndex index = sourceModel()->indexFromEntry(entries_.first()); scrollTo(proxyModel->mapFromSource(index)); } bool DetailedListView::eventFilter(QObject* obj_, QEvent* event_) { if(event_->type() == QEvent::ContextMenu && obj_ == header()) { m_currentContextColumn = header()->logicalIndexAt(static_cast(event_)->pos()); m_headerMenu->exec(static_cast(event_)->globalPos()); return true; } return GUI::TreeView::eventFilter(obj_, event_); } void DetailedListView::slotDoubleClicked(const QModelIndex& index_) { Data::EntryPtr entry = index_.data(EntryPtrRole).value(); if(entry) { Controller::self()->editEntry(entry); } } void DetailedListView::slotColumnMenuActivated(QAction* action_) { const int col = action_->data().toInt(); if(col > -1) { // only column actions have data const bool isChecked = action_->isChecked(); setColumnHidden(col, !isChecked); // if we're showing a column, resize all sections if(isChecked) { resizeColumnToContents(col); adjustColumnWidths(); } } checkHeader(); } void DetailedListView::showAllColumns() { foreach(QAction* action, m_columnMenu->actions()) { if(action->isCheckable() && !action->isChecked()) { action->trigger(); } } } void DetailedListView::hideAllColumns() { for(int ncol = 0; ncol < header()->count(); ++ncol) { hideColumn(ncol); } foreach(QAction* action, m_columnMenu->actions()) { if(action->isCheckable()) { action->setChecked(false); } } checkHeader(); } void DetailedListView::hideCurrentColumn() { setColumnHidden(m_currentContextColumn, true); checkHeader(); } void DetailedListView::slotRefresh() { sortModel()->invalidate(); } void DetailedListView::setFilter(Tellico::FilterPtr filter_) { static_cast(sortModel())->setFilter(filter_); } Tellico::FilterPtr DetailedListView::filter() const { return static_cast(sortModel())->filter(); } void DetailedListView::addField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr field) { sourceModel()->addFields(Data::FieldList() << field); } void DetailedListView::modifyField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr oldField_, Tellico::Data::FieldPtr newField_) { Q_UNUSED(oldField_) sourceModel()->modifyField(oldField_, newField_); } void DetailedListView::removeField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr field_) { sourceModel()->removeFields(Data::FieldList() << field_); } void DetailedListView::reorderFields(const Tellico::Data::FieldList& fields_) { QStringList columnNames; QList columnWidths, columnOrder; for(int ncol = 0; ncol < header()->count(); ++ncol) { // ignore hidden columns if(!isColumnHidden(ncol)) { columnNames << columnFieldName(ncol); columnWidths << columnWidth(ncol); columnOrder << header()->visualIndex(ncol); } } sourceModel()->setFields(fields_); QList currentColumnOrder; // now restore widths and order for(int ncol = 0; ncol < header()->count(); ++ncol) { int idx = columnNames.indexOf(columnFieldName(ncol)); // column width of 0 means hidden if(idx < 0 || columnWidths.at(idx) <= 0) { hideColumn(ncol); if(idx > -1) { currentColumnOrder << ncol; } } else { setColumnWidth(ncol, columnWidths.at(idx)); currentColumnOrder << ncol; } } const int maxCount = qMin(currentColumnOrder.size(), columnOrder.size()); for(int i = 0; i < maxCount; ++i) { header()->moveSection(header()->visualIndex(currentColumnOrder.at(i)), columnOrder.at(i)); } updateHeaderMenu(); } void DetailedListView::saveConfig(Tellico::Data::CollPtr coll_, int configIndex_) { const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_)); KConfigGroup config(KSharedConfig::openConfig(), configGroup); // all of this is to have custom settings on a per file basis QString configN; if(coll_->type() == Data::Collection::Base) { QList info; for(int i = 0; i < Config::maxCustomURLSettings(); ++i) { QUrl u(config.readEntry(QStringLiteral("URL_%1").arg(i))); if(!u.isEmpty() && i != configIndex_) { configN = QStringLiteral("_%1").arg(i); ConfigInfo ci; ci.cols = config.readEntry(QLatin1String("ColumnNames") + configN, QStringList()); ci.widths = config.readEntry(QLatin1String("ColumnWidths") + configN, QList()); ci.order = config.readEntry(QLatin1String("ColumnOrder") + configN, QList()); ci.prevSort = config.readEntry(QLatin1String("PrevSortColumn") + configN, 0); ci.prev2Sort = config.readEntry(QLatin1String("Prev2SortColumn") + configN, 0); ci.sortOrder = config.readEntry(QLatin1String("SortOrder") + configN, static_cast(Qt::AscendingOrder)); info.append(ci); } } // subtract one since we're writing the current settings, too int limit = qMin(info.count(), Config::maxCustomURLSettings()-1); for(int i = 0; i < limit; ++i) { // starts at one since the current config will be written below configN = QStringLiteral("_%1").arg(i+1); config.writeEntry(QLatin1String("ColumnNames") + configN, info[i].cols); config.writeEntry(QLatin1String("ColumnWidths") + configN, info[i].widths); config.writeEntry(QLatin1String("ColumnOrder") + configN, info[i].order); config.writeEntry(QLatin1String("PrevSortColumn") + configN, info[i].prevSort); config.writeEntry(QLatin1String("Prev2SortColumn") + configN, info[i].prev2Sort); config.writeEntry(QLatin1String("SortOrder") + configN, info[i].sortOrder); // legacy entry item config.deleteEntry(QLatin1String("ColumnState") + configN); } configN = QStringLiteral("_0"); } QStringList colNames; QList widths, order; for(int ncol = 0; ncol < header()->count(); ++ncol) { // ignore hidden columns if(!isColumnHidden(ncol)) { colNames << columnFieldName(ncol); widths << columnWidth(ncol); order << header()->visualIndex(ncol); } } config.writeEntry(QLatin1String("ColumnNames") + configN, colNames); config.writeEntry(QLatin1String("ColumnWidths") + configN, widths); config.writeEntry(QLatin1String("ColumnOrder") + configN, order); const int sortCol1 = sortModel()->sortColumn(); const int sortCol2 = sortModel()->secondarySortColumn(); const int sortCol3 = sortModel()->tertiarySortColumn(); const int sortOrder = static_cast(sortModel()->sortOrder()); config.writeEntry(QLatin1String("SortColumn") + configN, sortCol1); config.writeEntry(QLatin1String("PrevSortColumn") + configN, sortCol2); config.writeEntry(QLatin1String("Prev2SortColumn") + configN, sortCol3); config.writeEntry(QLatin1String("SortOrder") + configN, sortOrder); // remove old entry item config.deleteEntry(QLatin1String("ColumnState") + configN); } QString DetailedListView::sortColumnTitle1() const { return model()->headerData(header()->sortIndicatorSection(), Qt::Horizontal).toString(); } QString DetailedListView::sortColumnTitle2() const { return model()->headerData(sortModel()->secondarySortColumn(), Qt::Horizontal).toString(); } QString DetailedListView::sortColumnTitle3() const { return model()->headerData(sortModel()->tertiarySortColumn(), Qt::Horizontal).toString(); } QStringList DetailedListView::visibleColumns() const { // we want the visual order, so use a QMap and sort by visualIndex QMap titleMap; for(int i = 0; i < header()->count(); ++i) { if(!isColumnHidden(i)) { titleMap.insert(header()->visualIndex(i), model()->headerData(i, Qt::Horizontal).toString()); } } return titleMap.values(); } // can't be const Tellico::Data::EntryList DetailedListView::visibleEntries() { // We could just return the full collection entry list if the filter is 0 // but printing depends on the sorted order Data::EntryList entries; for(int i = 0; i < model()->rowCount(); ++i) { Data::EntryPtr tmp = model()->data(model()->index(i, 0), EntryPtrRole).value(); if(tmp) { entries += tmp; } } return entries; } void DetailedListView::selectAllVisible() { QModelIndex topLeft = model()->index(0, 0); QModelIndex bottomRight = model()->index(model()->rowCount()-1, model()->columnCount()-1); QItemSelection selection(topLeft, bottomRight); selectionModel()->select(selection, QItemSelectionModel::Select); } int DetailedListView::visibleItems() const { return model()->rowCount(); } void DetailedListView::resetEntryStatus() { sourceModel()->clearSaveState(); } void DetailedListView::updateHeaderMenu() { // we only want to update the menu when the header count and model count agree if(model()->columnCount() != header()->count()) { myDebug() << "column counts disagree"; return; } m_headerMenu->clear(); m_headerMenu->addSection(i18n("View Columns")); m_columnMenu->clear(); for(int ncol = 0; ncol < header()->count(); ++ncol) { Data::FieldPtr field = model()->headerData(ncol, Qt::Horizontal, FieldPtrRole).value(); if(field && (field->type() == Data::Field::Table || field->type() == Data::Field::Para)) { continue; } QAction* act = m_columnMenu->addAction(model()->headerData(ncol, Qt::Horizontal).toString()); act->setData(ncol); act->setCheckable(true); act->setChecked(!isColumnHidden(ncol)); } QAction* columnAction = m_headerMenu->addMenu(m_columnMenu); columnAction->setText(i18nc("Noun, Menu name", "Columns")); columnAction->setIcon(QIcon::fromTheme(QStringLiteral("view-file-columns"))); QAction* actHideThis = m_headerMenu->addAction(i18n("Hide This Column")); - connect(actHideThis, SIGNAL(triggered(bool)), this, SLOT(hideCurrentColumn())); + connect(actHideThis, &QAction::triggered, this, &DetailedListView::hideCurrentColumn); QAction* actResize = m_headerMenu->addAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Resize to Content")); - connect(actResize, SIGNAL(triggered(bool)), this, SLOT(resizeColumnsToContents())); + connect(actResize, &QAction::triggered, this, &DetailedListView::resizeColumnsToContents); m_headerMenu->addSeparator(); QAction* actShowAll = m_headerMenu->addAction(i18n("Show All Columns")); - connect(actShowAll, SIGNAL(triggered(bool)), this, SLOT(showAllColumns())); + connect(actShowAll, &QAction::triggered, this, &DetailedListView::showAllColumns); QAction* actHideAll = m_headerMenu->addAction(i18n("Hide All Columns")); - connect(actHideAll, SIGNAL(triggered(bool)), this, SLOT(hideAllColumns())); + connect(actHideAll, &QAction::triggered, this, &DetailedListView::hideAllColumns); } void DetailedListView::updateColumnDelegates() { for(int ncol = 0; ncol < header()->count(); ++ncol) { Data::FieldPtr field = model()->headerData(ncol, Qt::Horizontal, FieldPtrRole).value(); if(field && field->type() == Data::Field::Rating) { /// if we're not using the overall delegate, delete the delegate since we're setting a new on if(itemDelegateForColumn(ncol) != itemDelegate()) { delete itemDelegateForColumn(ncol); } RatingDelegate* delegate = new RatingDelegate(this); bool ok; // not used delegate->setMaxRating(Tellico::toUInt(field->property(QStringLiteral("maximum")), &ok)); setItemDelegateForColumn(ncol, delegate); } else { // reset column delegate to overall delegate setItemDelegateForColumn(ncol, itemDelegate()); } } } void DetailedListView::slotRefreshImages() { sourceModel()->setImagesAreAvailable(true); } void DetailedListView::adjustColumnWidths() { // this function is called when a column is shown // reduce all visible columns to their size hint, if they are wider than that for(int ncol = 0; ncol < header()->count(); ++ncol) { if(!isColumnHidden(ncol)) { const int width = sizeHintForColumn(ncol); if(columnWidth(ncol) > width) { resizeColumnToContents(ncol); } } } } void DetailedListView::resizeColumnsToContents() { for(int ncol = 0; ncol < header()->count(); ++ncol) { if(!isColumnHidden(ncol)) { resizeColumnToContents(ncol); } } } void DetailedListView::hideNewColumn(const QModelIndex& index_, int start_, int end_) { Q_UNUSED(index_); for(int ncol = start_; ncol <= end_; ++ncol) { hideColumn(ncol); } updateHeaderMenu(); // make sure to update checkable actions } void DetailedListView::checkHeader() { // the header disappears if all columns are hidden, so if the user hides all // columns, we turn around and show the title // // normally, I would expect a check like header()->count() == header()->hiddenSectionCount() // to tell me if all sections are hidden, but it often doesn't work, with the hiddenSectionCount() // being greater than count()! From testing, if the sizeHint() width is 0, then the header is hidden if(!header()->sizeHint().isEmpty()) { return; } // find title action in menu and activate it QAction* action = nullptr; QAction* fallbackAction = nullptr; foreach(QAction* tryAction, m_columnMenu->actions()) { const int ncol = tryAction->data().toInt(); if(ncol > -1 && columnFieldName(ncol) == QLatin1String("title")) { action = tryAction; break; } else if(ncol > -1 && !fallbackAction) { fallbackAction = tryAction; } } if(!action) { action = fallbackAction; } if(action) { action->setChecked(true); const int col = action->data().toInt(); // calling slotColumnMenuActivated() would be infinite loop setColumnHidden(col, false); resizeColumnToContents(col); } else { myDebug() << "found no action to show, still empty header!"; } } QString DetailedListView::columnFieldName(int ncol_) const { Data::FieldPtr field = model()->headerData(ncol_, Qt::Horizontal, FieldPtrRole).value(); return field ? field->name() : QString(); } diff --git a/src/detailedlistview.h b/src/detailedlistview.h index eea1d1b5..de3513c2 100644 --- a/src/detailedlistview.h +++ b/src/detailedlistview.h @@ -1,174 +1,173 @@ /*************************************************************************** Copyright (C) 2001-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 TELLICO_DETAILEDLISTVIEW_H #define TELLICO_DETAILEDLISTVIEW_H #include "gui/treeview.h" #include "observer.h" #include "filter.h" #include #include #include class QMenu; namespace Tellico { class DetailedEntryItem; class EntryModel; /** * The DetailedListView class shows detailed information about entries in the * collection. * * @author Robby Stephenson */ class DetailedListView : public GUI::TreeView, public Observer { Q_OBJECT public: /** * The constructor initializes the popup menu, but no columns are inserted. * * @param parent A pointer to the parent widget */ DetailedListView(QWidget* parent); virtual ~DetailedListView(); EntryModel* sourceModel() const; /** * Event filter used to popup the menu */ bool eventFilter(QObject* obj, QEvent* ev) Q_DECL_OVERRIDE; /** * Selects the item which refers to a certain entry. * * @param entry A pointer to the entry */ void setEntriesSelected(Data::EntryList entries); void setFilter(FilterPtr filter); FilterPtr filter() const; QString sortColumnTitle1() const; QString sortColumnTitle2() const; QString sortColumnTitle3() const; QStringList visibleColumns() const; Data::EntryList visibleEntries(); /** * @param coll A pointer to the collection */ void addCollection(Data::CollPtr coll); /** * Removes all items which refers to a entry within a collection. * * @param coll A pointer to the collection */ void removeCollection(Data::CollPtr coll); /** * Adds a new list item showing the details for a entry. * * @param entry A pointer to the entry */ virtual void addEntries(Data::EntryList entries) Q_DECL_OVERRIDE; /** * Modifies any item which refers to a entry, resetting the column contents. * * @param entry A pointer to the entry */ virtual void modifyEntries(Data::EntryList entries) Q_DECL_OVERRIDE; /** * Removes any item which refers to a certain entry. * * @param entry A pointer to the entry */ virtual void removeEntries(Data::EntryList entries) Q_DECL_OVERRIDE; virtual void addField(Data::CollPtr, Data::FieldPtr field) Q_DECL_OVERRIDE; void addField(Data::FieldPtr field, int width); virtual void modifyField(Data::CollPtr, Data::FieldPtr oldField, Data::FieldPtr newField) Q_DECL_OVERRIDE; virtual void removeField(Data::CollPtr, Data::FieldPtr field) Q_DECL_OVERRIDE; void reorderFields(const Data::FieldList& fields); /** * saveConfig is only needed for custom collections */ void saveConfig(Data::CollPtr coll, int saveConfig); /** * Select all visible items. */ void selectAllVisible(); int visibleItems() const; void resetEntryStatus(); public Q_SLOTS: /** * Resets the list view, clearing and deleting all items. */ void slotReset(); /** * Refreshes the view, repopulating all items. */ void slotRefresh(); void slotRefreshImages(); private Q_SLOTS: void slotDoubleClicked(const QModelIndex& index); void slotColumnMenuActivated(QAction* action); void updateHeaderMenu(); void showAllColumns(); void hideAllColumns(); void hideCurrentColumn(); void resizeColumnsToContents(); void hideNewColumn(const QModelIndex& index, int start, int end); // void slotCacheColumnWidth(int section, int oldSize, int newSize); void updateColumnDelegates(); private: void contextMenuEvent(QContextMenuEvent* event) Q_DECL_OVERRIDE; void setState(Tellico::Data::EntryList entries_, int state); void adjustColumnWidths(); void checkHeader(); QString columnFieldName(int ncol) const; struct ConfigInfo { QStringList cols; QList widths; QList order; int prevSort; int prev2Sort; int sortOrder; }; QMenu* m_headerMenu; QMenu* m_columnMenu; bool m_loadingCollection; - bool m_selectionChanging; int m_currentContextColumn; }; } // end namespace; #endif diff --git a/src/document.cpp b/src/document.cpp index 7cf1460e..28052ad1 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -1,851 +1,855 @@ /*************************************************************************** Copyright (C) 2001-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 . * * * ***************************************************************************/ #include "document.h" #include "collectionfactory.h" #include "translators/tellicoimporter.h" #include "translators/tellicozipexporter.h" #include "translators/tellicoxmlexporter.h" #include "collection.h" #include "core/filehandler.h" #include "borrower.h" #include "fieldformat.h" #include "core/tellico_strings.h" #include "images/imagefactory.h" #include "images/imagedirectory.h" #include "images/image.h" #include "images/imageinfo.h" #include "utils/stringset.h" #include "progressmanager.h" #include "config/tellico_config.h" #include "entrycomparison.h" #include "utils/guiproxy.h" #include "tellico_debug.h" #include #include #include #include #include #include using namespace Tellico; using Tellico::Data::Document; Document* Document::s_self = nullptr; Document::Document() : QObject(), m_coll(nullptr), m_isModified(false), m_loadAllImages(false), m_validFile(false), m_importer(nullptr), m_cancelImageWriting(true), m_fileFormat(Import::TellicoImporter::Unknown) { m_allImagesOnDisk = Config::imageLocation() != Config::ImagesInFile; newDocument(Collection::Book); } Document::~Document() { delete m_importer; m_importer = nullptr; } Tellico::Data::CollPtr Document::collection() const { return m_coll; } void Document::setURL(const QUrl& url_) { m_url = url_; if(m_url.fileName() != i18n(Tellico::untitledFilename)) { ImageFactory::setLocalDirectory(m_url); EntryComparison::setDocumentUrl(m_url); } } -void Document::slotSetModified(bool modified_/*=true*/) { +void Document::setModified(bool modified_) { if(modified_ != m_isModified) { m_isModified = modified_; emit signalModified(m_isModified); } } +void Document::slotSetModified() { + setModified(true); +} + /** * Since QUndoStack emits cleanChanged(), the behavior is opposite * the document modified flag */ void Document::slotSetClean(bool clean_) { - slotSetModified(!clean_); + setModified(!clean_); } bool Document::newDocument(int type_) { if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } deleteContents(); m_coll = CollectionFactory::collection(type_, true); m_coll->setTrackGroups(true); emit signalCollectionAdded(m_coll); emit signalCollectionImagesLoaded(m_coll); - slotSetModified(false); + setModified(false); QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename)); setURL(url); m_validFile = false; m_fileFormat = Import::TellicoImporter::Unknown; return true; } bool Document::openDocument(const QUrl& url_) { MARK; m_loadAllImages = false; // delayed image loading only works for local files if(!url_.isLocalFile()) { m_loadAllImages = true; } if(m_importer) { m_importer->deleteLater(); } m_importer = new Import::TellicoImporter(url_, m_loadAllImages); 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); CollPtr coll = m_importer->collection(); if(!m_importer) { myDebug() << "The importer was deleted out from under us"; return false; } // delayed image loading only works for zip files // format is only known AFTER collection() is called m_fileFormat = m_importer->format(); m_allImagesOnDisk = !m_importer->hasImages(); if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) { m_loadAllImages = true; } ImageFactory::setZipArchive(m_importer->takeImages()); if(!coll) { // myDebug() << "returning false"; GUI::Proxy::sorry(m_importer->statusMessage()); m_validFile = false; return false; } deleteContents(); m_coll = coll; m_coll->setTrackGroups(true); setURL(url_); m_validFile = true; emit signalCollectionAdded(m_coll); // m_importer might have been deleted? - slotSetModified(m_importer && m_importer->modifiedOriginal()); + setModified(m_importer && m_importer->modifiedOriginal()); // if(pruneImages()) { // slotSetModified(true); // } if(m_importer && m_importer->hasImages()) { m_cancelImageWriting = false; - QTimer::singleShot(500, this, SLOT(slotLoadAllImages())); + QTimer::singleShot(500, this, &Document::slotLoadAllImages); } else { emit signalCollectionImagesLoaded(m_coll); if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } } return true; } bool Document::saveDocument(const QUrl& url_, bool force_) { // FileHandler::queryExists calls FileHandler::writeBackupFile // so the only reason to check queryExists() is if the url to write to is different than the current one if(url_ == m_url) { if(!FileHandler::writeBackupFile(url_)) { return false; } } else { if(!force_ && !FileHandler::queryExists(url_)) { return false; } } // in case we're still loading images, give that a chance to cancel m_cancelImageWriting = true; qApp->processEvents(); ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false); ProgressItem::Done done(this); // will always save as zip file, no matter if has images or not int imageLocation = Config::imageLocation(); bool includeImages = imageLocation == Config::ImagesInFile; int totalSteps; // write all images to disk cache if needed // have to do this before executing exporter in case // the user changed the imageInFile setting from Yes to No, in which // case saving will overwrite the old file that has the images in it! if(includeImages) { totalSteps = 10; item.setTotalSteps(totalSteps); // since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress } else { totalSteps = 100; item.setTotalSteps(totalSteps); m_cancelImageWriting = false; writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_); } QScopedPointer exporter; if(m_fileFormat == Import::TellicoImporter::XML) { exporter.reset(new Export::TellicoXMLExporter(m_coll)); static_cast(exporter.data())->setIncludeImages(includeImages); } else { exporter.reset(new Export::TellicoZipExporter(m_coll)); static_cast(exporter.data())->setIncludeImages(includeImages); } item.setProgress(int(0.8*totalSteps)); exporter->setEntries(m_coll->entries()); exporter->setURL(url_); // since we already asked about overwriting the file, force the save long opt = exporter->options() | Export::ExportForce | Export::ExportComplete | Export::ExportProgress; // only write the image sizes if they're known already opt &= ~Export::ExportImageSize; exporter->setOptions(opt); const bool success = exporter->exec(); item.setProgress(int(0.9*totalSteps)); if(success) { setURL(url_); // if successful, doc is no longer modified - slotSetModified(false); + setModified(false); } else { myDebug() << "Document::saveDocument() - not successful saving to" << url_.url(); } return success; } bool Document::closeDocument() { if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } deleteContents(); return true; } void Document::deleteContents() { if(m_coll) { emit signalCollectionDeleted(m_coll); } // don't delete the m_importer here, bad things will happen // since the collection holds a pointer to each entry and each entry // hold a pointer to the collection, and they're both sharedptrs, // neither will ever get deleted, unless the entries are removed from the collection if(m_coll) { m_coll->clear(); } m_coll = nullptr; // old collection gets deleted as refcount goes to 0 m_cancelImageWriting = true; } void Document::appendCollection(Tellico::Data::CollPtr coll_) { appendCollection(m_coll, coll_); } void Document::appendCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_) { if(!coll1_ || !coll2_) { return; } coll1_->blockSignals(true); foreach(FieldPtr field, coll2_->fields()) { coll1_->mergeField(field); } Data::EntryList newEntries; foreach(EntryPtr entry, coll2_->entries()) { Data::EntryPtr newEntry(new Data::Entry(*entry)); newEntry->setCollection(coll1_); newEntries << newEntry; } coll1_->addEntries(newEntries); // TODO: merge filters and loans coll1_->blockSignals(false); } Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll_) { return mergeCollection(m_coll, coll_); } Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_) { MergePair pair; if(!coll1_ || !coll2_) { return pair; } coll1_->blockSignals(true); Data::FieldList fields = coll2_->fields(); foreach(FieldPtr field, fields) { coll1_->mergeField(field); } EntryList currEntries = coll1_->entries(); EntryList newEntries = coll2_->entries(); std::sort(currEntries.begin(), currEntries.end(), Data::EntryCmp(QStringLiteral("title"))); std::sort(newEntries.begin(), newEntries.end(), Data::EntryCmp(QStringLiteral("title"))); const int currTotal = currEntries.count(); int lastMatchId = 0; bool checkSameId = false; // if the matching entries have the same id, then check that first for later comparisons foreach(EntryPtr newEntry, newEntries) { int bestMatch = 0; Data::EntryPtr matchEntry, currEntry; // first, if we're checking against same ID if(checkSameId) { currEntry = coll1_->entryById(newEntry->id()); if(currEntry && coll1_->sameEntry(currEntry, newEntry) >= EntryComparison::ENTRY_PERFECT_MATCH) { // only have to compare against perfect match matchEntry = currEntry; } } if(!matchEntry) { // alternative is to loop over them all for(int i = 0; i < currTotal; ++i) { // since we're sorted by title, track the index of the previous match and start comparison there currEntry = currEntries.at((i+lastMatchId) % currTotal); const int match = coll1_->sameEntry(currEntry, newEntry); if(match >= EntryComparison::ENTRY_PERFECT_MATCH) { matchEntry = currEntry; lastMatchId = (i+lastMatchId) % currTotal; break; } else if(match >= EntryComparison::ENTRY_GOOD_MATCH && match > bestMatch) { bestMatch = match; matchEntry = currEntry; lastMatchId = (i+lastMatchId) % currTotal; // don't break, keep looking for better one } } } if(matchEntry) { checkSameId = checkSameId || (matchEntry->id() == newEntry->id()); mergeEntry(matchEntry, newEntry); } else { Data::EntryPtr e(new Data::Entry(*newEntry)); e->setCollection(coll1_); // keep track of which entries got added pair.first.append(e); } } coll1_->addEntries(pair.first); // TODO: merge filters and loans coll1_->blockSignals(false); return pair; } void Document::replaceCollection(Tellico::Data::CollPtr coll_) { if(!coll_) { return; } QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename)); setURL(url); m_validFile = false; // the collection gets cleared by the CollectionCommand that called this function // no need to do it here m_coll = coll_; m_coll->setTrackGroups(true); m_cancelImageWriting = true; // CollectionCommand takes care of calling Controller signals } void Document::unAppendCollection(Tellico::Data::CollPtr coll_, Tellico::Data::FieldList origFields_) { if(!coll_) { return; } m_coll->blockSignals(true); StringSet origFieldNames; foreach(FieldPtr field, origFields_) { m_coll->modifyField(field); origFieldNames.add(field->name()); } EntryList entries = coll_->entries(); foreach(EntryPtr entry, entries) { // probably don't need to do this, but on the safe side... entry->setCollection(coll_); } m_coll->removeEntries(entries); // since Collection::removeField() iterates over all entries to reset the value of the field // don't removeField() until after removeEntry() is done FieldList currFields = m_coll->fields(); foreach(FieldPtr field, currFields) { if(!origFieldNames.has(field->name())) { m_coll->removeField(field); } } m_coll->blockSignals(false); } void Document::unMergeCollection(Tellico::Data::CollPtr coll_, Tellico::Data::FieldList origFields_, Tellico::Data::MergePair entryPair_) { if(!coll_) { return; } m_coll->blockSignals(true); QStringList origFieldNames; foreach(FieldPtr field, origFields_) { m_coll->modifyField(field); origFieldNames << field->name(); } // first item in pair are the entries added by the operation, remove them EntryList entries = entryPair_.first; m_coll->removeEntries(entries); // second item in pair are the entries which got modified by the original merge command const QString track = QStringLiteral("track"); PairVector trackChanges = entryPair_.second; // need to go through them in reverse since one entry may have been modified multiple times // first item in the pair is the entry pointer // second item is the old value of the track field for(int i = trackChanges.count()-1; i >= 0; --i) { trackChanges[i].first->setField(track, trackChanges[i].second); } // since Collection::removeField() iterates over all entries to reset the value of the field // don't removeField() until after removeEntry() is done FieldList currFields = m_coll->fields(); foreach(FieldPtr field, currFields) { if(origFieldNames.indexOf(field->name()) == -1) { m_coll->removeField(field); } } m_coll->blockSignals(false); } bool Document::isEmpty() const { //an empty doc may contain a collection, but no entries return (!m_coll || m_coll->entries().isEmpty()); } bool Document::loadAllImagesNow() const { // DEBUG_LINE; if(!m_coll || !m_validFile) { return false; } if(m_loadAllImages) { myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!"; return false; } return Import::TellicoImporter::loadAllImages(m_url); } Tellico::Data::EntryList Document::filteredEntries(Tellico::FilterPtr filter_) const { Data::EntryList matches; Data::EntryList entries = m_coll->entries(); foreach(EntryPtr entry, entries) { if(filter_->matches(entry)) { matches.append(entry); } } return matches; } void Document::checkOutEntry(Tellico::Data::EntryPtr entry_) { if(!entry_) { return; } const QString loaned = QStringLiteral("loaned"); if(!m_coll->hasField(loaned)) { FieldPtr f(new Field(loaned, i18n("Loaned"), Field::Bool)); f->setFlags(Field::AllowGrouped); f->setCategory(i18n("Personal")); m_coll->addField(f); } entry_->setField(loaned, QStringLiteral("true")); EntryList vec; vec.append(entry_); m_coll->updateDicts(vec, QStringList() << loaned); } void Document::checkInEntry(Tellico::Data::EntryPtr entry_) { if(!entry_) { return; } const QString loaned = QStringLiteral("loaned"); if(!m_coll->hasField(loaned)) { return; } entry_->setField(loaned, QString()); m_coll->updateDicts(EntryList() << entry_, QStringList() << loaned); } void Document::renameCollection(const QString& newTitle_) { m_coll->setTitle(newTitle_); } // this only gets called when a zip file with images is opened // by loading every image, it gets pulled out of the zip file and // copied to disk. Then the zip file can be closed and not retained in memory void Document::slotLoadAllImages() { QString id; StringSet images; foreach(EntryPtr entry, m_coll->entries()) { foreach(FieldPtr field, m_coll->imageFields()) { id = entry->field(field); if(id.isEmpty() || images.has(id)) { continue; } // this is the early loading, so just by calling imageById() // the image gets sucked from the zip file and written to disk // by ImageFactory::imageById() // TODO:: does this need to check against images with link only? if(ImageFactory::imageById(id).isNull()) { myDebug() << "Null image for entry:" << entry->title() << id; } images.add(id); if(m_cancelImageWriting) { break; } } if(m_cancelImageWriting) { break; } // stay responsive, do this in the background qApp->processEvents(); } if(m_cancelImageWriting) { myLog() << "slotLoadAllImages() - cancel image writing"; } else { emit signalCollectionImagesLoaded(m_coll); } m_cancelImageWriting = false; if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } } // cacheDir_ is the location dir to write the images // localDir_ provide the new file location which is only needed if cacheDir == LocalDir void Document::writeAllImages(int cacheDir_, const QUrl& localDir_) { // images get 80 steps in saveDocument() const uint stepSize = 1 + qMax(1, m_coll->entryCount()/80); // add 1 since it could round off uint j = 1; ImageFactory::CacheDir cacheDir = static_cast(cacheDir_); QScopedPointer imgDir; if(cacheDir == ImageFactory::LocalDir) { imgDir.reset(new ImageDirectory(ImageFactory::localDirectory(localDir_))); } QString id; StringSet images; EntryList entries = m_coll->entries(); FieldList imageFields = m_coll->imageFields(); foreach(EntryPtr entry, entries) { foreach(FieldPtr field, imageFields) { id = entry->field(field); if(id.isEmpty() || images.has(id)) { continue; } images.add(id); if(ImageFactory::imageInfo(id).linkOnly) { continue; } // careful here, if we're writing to LocalDir, need to read from the old LocalDir and write to new bool success; if(cacheDir == ImageFactory::LocalDir) { success = ImageFactory::writeCachedImage(id, imgDir.data()); } else { success = ImageFactory::writeCachedImage(id, cacheDir); } if(!success) { myDebug() << "did not write image for entry title:" << entry->title(); } if(m_cancelImageWriting) { break; } } if(j%stepSize == 0) { ProgressManager::self()->setProgress(this, j/stepSize); qApp->processEvents(); } ++j; if(m_cancelImageWriting) { break; } } if(m_cancelImageWriting) { myDebug() << "Document::writeAllImages() - cancel image writing"; } m_cancelImageWriting = false; } bool Document::pruneImages() { bool found = false; QString id; StringSet images; Data::EntryList entries = m_coll->entries(); Data::FieldList imageFields = m_coll->imageFields(); foreach(EntryPtr entry, entries) { foreach(FieldPtr field, imageFields) { id = entry->field(field); if(id.isEmpty() || images.has(id)) { continue; } const Data::Image& img = ImageFactory::imageById(id); if(img.isNull()) { entry->setField(field, QString()); found = true; myDebug() << "removing null image for" << entry->title() << ":" << id; } else { images.add(id); } } } return found; } int Document::imageCount() const { if(!m_coll) { return 0; } StringSet images; FieldList fields = m_coll->imageFields(); EntryList entries = m_coll->entries(); foreach(FieldPtr field, fields) { foreach(EntryPtr entry, entries) { images.add(entry->field(field->name())); } } return images.count(); } void Document::removeImagesNotInCollection(Tellico::Data::EntryList entries_, Tellico::Data::EntryList entriesToKeep_) { // first get list of all images in collection StringSet images; FieldList fields = m_coll->imageFields(); EntryList allEntries = m_coll->entries(); foreach(FieldPtr field, fields) { foreach(EntryPtr entry, allEntries) { images.add(entry->field(field->name())); } foreach(EntryPtr entry, entriesToKeep_) { images.add(entry->field(field->name())); } } // now for all images not in the cache, we can clear them StringSet imagesToCheck = ImageFactory::imagesNotInCache(); // if entries_ is not empty, that means we want to limit the images removed // to those that are referenced in those entries StringSet imagesToRemove; foreach(FieldPtr field, fields) { foreach(EntryPtr entry, entries_) { QString id = entry->field(field->name()); if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) { imagesToRemove.add(id); } } } const QStringList realImagesToRemove = imagesToRemove.toList(); for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) { ImageFactory::removeImage(*it, false); // doesn't delete, just remove link } } bool Document::mergeEntry(Data::EntryPtr e1, Data::EntryPtr e2, MergeConflictResolver* resolver_) { if(!e1 || !e2) { myDebug() << "bad entry pointer"; return false; } bool ret = true; foreach(FieldPtr field, e1->collection()->fields()) { if(e2->field(field).isEmpty()) { continue; } // never try to merge entry id, creation date or mod date. Those are unique to each entry if(field->name() == QLatin1String("id") || field->name() == QLatin1String("cdate") || field->name() == QLatin1String("mdate")) { continue; } // myLog() << "reading field: " << field->name(); if(e1->field(field) == e2->field(field)) { continue; } else if(e1->field(field).isEmpty()) { // myLog() << e1->title() << ": updating field(" << field->name() << ") to " << e2->field(field); e1->setField(field, e2->field(field)); ret = true; } else if(field->type() == Data::Field::Table) { // if field F is a table-type field (album tracks, files, etc.), merge rows (keep their position) // if e1's F val in [row i, column j] empty, replace with e2's val at same position // if different (non-empty) vals at same position, CONFLICT! QStringList vals1 = FieldFormat::splitTable(e1->field(field)); QStringList vals2 = FieldFormat::splitTable(e2->field(field)); while(vals1.count() < vals2.count()) { vals1 += QString(); } for(int i = 0; i < vals2.count(); ++i) { if(vals2[i].isEmpty()) { continue; } if(vals1[i].isEmpty()) { vals1[i] = vals2[i]; ret = true; } else { QStringList parts1 = FieldFormat::splitRow(vals1[i]); QStringList parts2 = FieldFormat::splitRow(vals2[i]); bool changedPart = false; while(parts1.count() < parts2.count()) { parts1 += QString(); } for(int j = 0; j < parts2.count(); ++j) { if(parts2[j].isEmpty()) { continue; } if(parts1[j].isEmpty()) { parts1[j] = parts2[j]; changedPart = true; } else if(resolver_ && parts1[j] != parts2[j]) { int resolverResponse = resolver_->resolve(e1, e2, field, parts1[j], parts2[j]); if(resolverResponse == MergeConflictResolver::CancelMerge) { ret = false; return false; // cancel all the merge right now } else if(resolverResponse == MergeConflictResolver::KeepSecond) { parts1[j] = parts2[j]; changedPart = true; } } } if(changedPart) { vals1[i] = parts1.join(FieldFormat::columnDelimiterString()); ret = true; } } } if(ret) { e1->setField(field, vals1.join(FieldFormat::rowDelimiterString())); } // remove the merging due to user comments // maybe in the future have a more intelligent way #if 0 } else if(field->hasFlag(Data::Field::AllowMultiple)) { // if field F allows multiple values and not a Table (see above case), // e1's F values = (e1's F values) U (e2's F values) (union) // replace e1's field with union of e1's and e2's values for this field QStringList items1 = e1->fields(field, false); QStringList items2 = e2->fields(field, false); foreach(const QString& item2, items2) { // possible to have one value formatted and the other one not... if(!items1.contains(item2) && !items1.contains(Field::format(item2, field->formatType()))) { items1.append(item2); } } // not sure if I think it should be sorted or not // items1.sort(); e1->setField(field, items1.join(FieldFormat::delimiterString())); ret = true; #endif } else if(resolver_) { const int resolverResponse = resolver_->resolve(e1, e2, field); if(resolverResponse == MergeConflictResolver::CancelMerge) { ret = false; // we got cancelled return false; // cancel all the merge right now } else if(resolverResponse == MergeConflictResolver::KeepSecond) { e1->setField(field, e2->field(field)); } } else { // myDebug() << "Keeping value of" << field->name() << "for" << e1->field(QStringLiteral("title")); } } return ret; } //static QPair Document::mergeFields(Data::CollPtr coll_, Data::FieldList fields_, Data::EntryList entries_) { Data::FieldList modified, created; foreach(Data::FieldPtr field, fields_) { // don't add a field if it's a default field and not in the current collection if(coll_->hasField(field->name()) || CollectionFactory::isDefaultField(coll_->type(), field->name())) { // special case for choice fields, since we might want to add a value if(field->type() == Data::Field::Choice && coll_->hasField(field->name())) { // a2 are the existing fields in the collection, keep them in the same order QStringList a1 = coll_->fieldByName(field->name())->allowed(); foreach(const QString& newAllowedValue, field->allowed()) { if(!a1.contains(newAllowedValue)) { // could be slow for large merges, but we do only want to add new value // IF that value is actually used by an entry foreach(Data::EntryPtr entry, entries_) { if(entry->field(field->name()) == newAllowedValue) { a1 += newAllowedValue; break; } } } } if(a1.count() != coll_->fieldByName(field->name())->allowed().count()) { Data::FieldPtr f(new Data::Field(*coll_->fieldByName(field->name()))); f->setAllowed(a1); modified.append(f); } } continue; } // add field if any values are not empty foreach(Data::EntryPtr entry, entries_) { if(!entry->field(field).isEmpty()) { created.append(Data::FieldPtr(new Data::Field(*field))); break; } } } return qMakePair(modified, created); } diff --git a/src/document.h b/src/document.h index 3337d0a6..46b08e10 100644 --- a/src/document.h +++ b/src/document.h @@ -1,253 +1,253 @@ /*************************************************************************** Copyright (C) 2001-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 TELLICO_DOCUMENT_H #define TELLICO_DOCUMENT_H #include "datavectors.h" #include "filter.h" #include #include #include namespace Tellico { namespace Import { class TellicoImporter; class TellicoSaxImporter; } class MergeConflictResolver { public: enum Result { KeepFirst, KeepSecond, CancelMerge }; MergeConflictResolver() {} virtual ~MergeConflictResolver() {} virtual Result resolve(Data::EntryPtr entry1, Data::EntryPtr entry2, Data::FieldPtr field, const QString& value1 = QString(), const QString& value2 = QString()) = 0; }; namespace Data { /** * The Document contains everything needed to deal with the contents, thus separated from * the viewer, the Tellico object. It can take of opening and saving documents, and contains * a list of the collections in the document. * * @author Robby Stephenson */ class Document : public QObject { Q_OBJECT public: static Document* self() { if(!s_self) s_self = new Document(); return s_self; } /** * Sets the URL associated with the document. * * @param url The URL */ void setURL(const QUrl& url); /** * Checks the modified flag, which indicates if the document has changed since the * last save. * * @return A boolean indicating the modified status */ bool isModified() const { return m_isModified; } + void setModified(bool modified); /** * Sets whether all images are loaded from file or not */ void setLoadAllImages(bool loadAll) { m_loadAllImages = loadAll; } /** * Returns the current url associated with the document * * @return The url */ const QUrl& URL() const { return m_url; } /** * Initializes a new document. The signalNewDoc() signal is emitted. The return * value is currently always true, but should indicate whether or not a new document * was correctly initialized. * * @param type The type of collection to add * @return A boolean indicating success */ bool newDocument(int type); /** * Open a document given a specified location. If, for whatever reason, the file * cannot be opened, a proper message box is shown, indicating the problem. The * signalNewDoc() signal is made once the file contents have been confirmed. * * @param url The location to open * @return A boolean indicating success */ bool openDocument(const QUrl& url); /** * Saves the document contents to a file. * * @param url The location to save the file * @param force Boolean indicating the file should be overwritten if necessary * @return A boolean indicating success */ bool saveDocument(const QUrl& url, bool force = false); /** * Closes the document, deleting the contents. The return value is presently always true. * * @return A boolean indicating success */ bool closeDocument(); /** * Deletes the contents of the document. A signalCollectionDeleted() will be sent for every * collection in the document. */ void deleteContents(); /** * Returns a pointer to the document collection * * @return The collection */ CollPtr collection() const; /** * Returns true if there are no entries. A doc with an empty collection is still empty. */ bool isEmpty() const; /** * Appends the contents of another collection to the current one. The collections must be the * same type. Fields which are in the current collection are left alone. Fields * in the appended collection not in the current one are added. Entries in the appended collection * are added to the current one. * * @param coll A pointer to the appended collection. */ void appendCollection(CollPtr coll); static void appendCollection(CollPtr targetColl, CollPtr sourceColl); /** * Merges another collection into this one. The collections must be the same type. Fields in the * current collection are left alone. Fields not in the current are added. The merging is slow * since each entry in @p coll must be compared to every entry in the current collection. * * @param coll A pointer to the collection to be merged. * @return A QPair of the merged entries, see note in datavectors.h */ MergePair mergeCollection(CollPtr coll); static MergePair mergeCollection(CollPtr targetColl, CollPtr sourceColl); /** * Replace the current collection with a new one. Effectively, this is equivalent to opening * a new file containing this collection. * * @param coll A Pointer to the new collection, the document takes ownership. */ void replaceCollection(CollPtr coll); void unAppendCollection(CollPtr coll, FieldList origFields); void unMergeCollection(CollPtr coll, FieldList origFields_, MergePair entryPair); bool loadAllImagesNow() const; bool allImagesOnDisk() const { return m_allImagesOnDisk; } int imageCount() const; EntryList filteredEntries(FilterPtr filter) const; void renameCollection(const QString& newTitle); void checkInEntry(EntryPtr entry); void checkOutEntry(EntryPtr entry); /** * The second entry vector contains entries with images which should not be removed * in addition to those already in the collection */ void removeImagesNotInCollection(EntryList entries, EntryList entriesToKeep); void cancelImageWriting() { m_cancelImageWriting = true; } static bool mergeEntry(EntryPtr entry1, EntryPtr entry2, MergeConflictResolver* resolver=nullptr); // adds new fields into collection if any values in entries are not empty // first object is modified fields, second is new fields static QPair mergeFields(Data::CollPtr coll, Data::FieldList fields, Data::EntryList entries); public Q_SLOTS: /** - * Sets the modified flag. If it is true, the signalModified signal is made. + * Sets the modified flag to true, emitting signalModified. * - * @param m A boolean indicating the current modified status */ - void slotSetModified(bool m=true); + void slotSetModified(); void slotSetClean(bool clean); Q_SIGNALS: /** * Signals that the document has been modified. */ void signalModified(bool modified); /** * Signals that a status message should be shown. * * @param str The message */ void signalStatusMsg(const QString& str); /** * Signals that all images in the loaded file have been loaded * into memory or onto the disk */ void signalCollectionImagesLoaded(Tellico::Data::CollPtr coll); void signalCollectionAdded(Tellico::Data::CollPtr coll); void signalCollectionDeleted(Tellico::Data::CollPtr coll); private Q_SLOTS: /** * Does an initial loading of all images, used for writing * images to temp dir initially */ void slotLoadAllImages(); private: static Document* s_self; /** * Writes all images in the current collection to the cache directory * if cacheDir = LocalDir, then url will be used and must not be empty */ void writeAllImages(int cacheDir, const QUrl& url=QUrl()); bool pruneImages(); // make all constructors private Document(); Document(const Document& doc); Document& operator=(const Document&); ~Document(); CollPtr m_coll; bool m_isModified; bool m_loadAllImages; QUrl m_url; bool m_validFile; QPointer m_importer; bool m_cancelImageWriting; int m_fileFormat; bool m_allImagesOnDisk; }; } // end namespace } // end namespace #endif diff --git a/src/entryeditdialog.cpp b/src/entryeditdialog.cpp index 6844b37e..b516133d 100644 --- a/src/entryeditdialog.cpp +++ b/src/entryeditdialog.cpp @@ -1,791 +1,791 @@ /*************************************************************************** Copyright (C) 2001-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 . * * * ***************************************************************************/ #include "entryeditdialog.h" #include "gui/tabwidget.h" #include "collection.h" #include "controller.h" #include "field.h" #include "entry.h" #include "fieldformat.h" #include "tellico_kernel.h" #include "utils/cursorsaver.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // must be an even number static const int NCOLS = 2; // number of columns of GUI::FieldWidgets static const char* dialogOptionsString = "Edit Dialog Options"; } using Tellico::EntryEditDialog; EntryEditDialog::EntryEditDialog(QWidget* parent_) : QDialog(parent_), m_tabs(new GUI::TabWidget(this)), m_modified(false), m_isOrphan(false), m_isWorking(false), m_needReset(false) { setWindowTitle(i18n("Edit Entry")); QVBoxLayout* mainLayout = new QVBoxLayout(); setLayout(mainLayout); mainLayout->addWidget(m_tabs); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Help| QDialogButtonBox::Close| QDialogButtonBox::Apply); - connect(buttonBox, SIGNAL(helpRequested()), this, SLOT(slotHelp())); + connect(buttonBox, &QDialogButtonBox::helpRequested, this, &EntryEditDialog::slotHelp); mainLayout->addWidget(buttonBox); m_newButton = new QPushButton(); buttonBox->addButton(m_newButton, QDialogButtonBox::ActionRole); m_newButton->setDefault(true); KGuiItem::assign(m_newButton, KGuiItem(i18n("&New Entry"))); m_saveButton = buttonBox->button(QDialogButtonBox::Apply); m_saveButton->setEnabled(false); KGuiItem save = KStandardGuiItem::save(); save.setText(i18n("Sa&ve Entry")); KGuiItem::assign(m_saveButton, save); - connect(buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), SLOT(slotClose())); - connect(m_saveButton, SIGNAL(clicked()), SLOT(slotHandleSave())); - connect(m_newButton, SIGNAL(clicked()), SLOT(slotHandleNew())); + connect(buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &EntryEditDialog::slotClose); + connect(m_saveButton, &QAbstractButton::clicked, this, &EntryEditDialog::slotHandleSave); + connect(m_newButton, &QAbstractButton::clicked, this, &EntryEditDialog::slotHandleNew); } EntryEditDialog::~EntryEditDialog() { } void EntryEditDialog::reject() { slotClose(); } void EntryEditDialog::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("entry-editor")); } void EntryEditDialog::slotClose() { // check to see if an entry should be saved before hiding // block signals so the entry view and selection isn't cleared if(m_modified && queryModified()) { accept(); // make sure to reset values in the dialog m_needReset = true; setContents(m_currEntries); slotSetModified(false); } else if(!m_modified) { accept(); } } void EntryEditDialog::slotReset() { if(m_isWorking) { return; } slotSetModified(false); m_saveButton->setEnabled(false); m_saveButton->setText(i18n("Sa&ve Entry")); m_currColl = nullptr; m_currEntries.clear(); while(m_tabs->count() > 0) { QWidget* widget = m_tabs->widget(0); m_tabs->removeTab(0); delete widget; } m_widgetDict.clear(); } void EntryEditDialog::resetLayout(Tellico::Data::CollPtr coll_) { if(!coll_ || m_isWorking) { return; } m_newButton->setIcon(QIcon(QLatin1String(":/icons/") + Kernel::self()->collectionTypeName())); setUpdatesEnabled(false); if(m_tabs->count() > 0) { // myDebug() << "resetting contents."; slotReset(); } m_isWorking = true; m_currColl = coll_; int maxHeight = 0; QList gridList; bool noChoices = true; bool focusedFirst = false; QStringList catList = m_currColl->fieldCategories(); for(QStringList::ConstIterator catIt = catList.constBegin(); catIt != catList.constEnd(); ++catIt) { Data::FieldList allCategoryfields = m_currColl->fieldsByCategory(*catIt); Data::FieldList fields; // remove fields which we don't plan to show foreach(Data::FieldPtr field, allCategoryfields) { // uneditabled and fields with derived values don't get widgets if(field->hasFlag(Data::Field::NoEdit) || field->hasFlag(Data::Field::Derived)) { continue; } fields << field; } if(fields.isEmpty()) { // sanity check continue; } // if this layout model is changed, be sure to check slotUpdateField() QWidget* page = new QWidget(m_tabs); QBoxLayout* boxLayout = new QVBoxLayout(page); QWidget* grid = new QWidget(page); gridList.append(grid); // spacing gets a bit weird, if there are absolutely no Choice fields, // then spacing should be 5, which is set later QGridLayout* layout = new QGridLayout(grid); boxLayout->addWidget(grid, 0); // those with multiple, get a stretch if(fields.count() > 1 || !fields[0]->isSingleCategory()) { boxLayout->addStretch(1); } // keep track of which should expand QVector expands(NCOLS, false); QVector maxWidth(NCOLS, 0); int count = 0; foreach(Data::FieldPtr field, fields) { if(field->type() == Data::Field::Choice) { noChoices = false; } GUI::FieldWidget* widget = GUI::FieldWidget::create(field, grid); if(!widget) { continue; } widget->insertDefault(); - connect(widget, SIGNAL(valueChanged(Tellico::Data::FieldPtr)), SLOT(fieldValueChanged(Tellico::Data::FieldPtr))); + connect(widget, &GUI::FieldWidget::valueChanged, this, &EntryEditDialog::fieldValueChanged); if(!focusedFirst && widget->focusPolicy() != Qt::NoFocus) { widget->setFocus(); focusedFirst = true; } int r = count/NCOLS; int c = count%NCOLS; layout->addWidget(widget, r, c); layout->setRowStretch(r, 1); m_widgetDict.insert(QString::number(m_currColl->id()) + field->name(), widget); maxWidth[count%NCOLS] = qMax(maxWidth[count%NCOLS], widget->labelWidth()); if(widget->expands()) { expands[count%NCOLS] = true; } widget->updateGeometry(); if(!field->isSingleCategory()) { maxHeight = qMax(maxHeight, widget->minimumSizeHint().height()); } ++count; } // now, the labels in a column should all be the same width count = 0; foreach(Data::FieldPtr field, fields) { GUI::FieldWidget* widget = m_widgetDict.value(QString::number(m_currColl->id()) + field->name()); if(widget) { widget->setLabelWidth(maxWidth[count%NCOLS]); ++count; } } // update stretch factors for columns with a line edit for(int col = 0; col < NCOLS; ++col) { if(expands[col]) { layout->setColumnStretch(col, 1); } } m_tabs->addTab(page, *catIt); } // Now, go through and set all the field widgets to the same height foreach(QWidget* grid, gridList) { QGridLayout* l = static_cast(grid->layout()); if(noChoices) { l->setSpacing(5); } for(int row = 0; row < l->rowCount() && grid->children().count() > 1; ++row) { l->setRowMinimumHeight(row, maxHeight); } // I don't want anything to be hidden, Keramik has a bug if I don't do this grid->setMinimumHeight(grid->sizeHint().height()); // the parent of the grid is the page that got added to the tabs grid->parentWidget()->layout()->invalidate(); grid->parentWidget()->setMinimumHeight(grid->parentWidget()->sizeHint().height()); // also, no accels for the field widgets KAcceleratorManager::setNoAccel(grid); } setUpdatesEnabled(true); // this doesn't seem to work // setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); // so do this instead // layout()->invalidate(); // needed so the sizeHint() gets recalculated m_tabs->setMinimumHeight(m_tabs->minimumSizeHint().height()); m_tabs->setMinimumWidth(m_tabs->sizeHint().width()); m_tabs->setCurrentIndex(0); m_isWorking = false; slotHandleNew(); } void EntryEditDialog::slotHandleNew() { if(!m_currColl || !queryModified()) { return; } m_tabs->setCurrentIndex(0); m_tabs->setFocusToFirstChild(); clear(); m_isWorking = true; // clear() will get called again if(!signalsBlocked()) { Controller::self()->slotClearSelection(); } m_isWorking = false; Data::EntryPtr entry(new Data::Entry(m_currColl)); m_currEntries.append(entry); m_isOrphan = true; } void EntryEditDialog::slotHandleSave() { if(!m_currColl || m_isWorking) { return; } m_isWorking = true; if(m_currEntries.isEmpty()) { myDebug() << "creating new entry"; m_currEntries.append(Data::EntryPtr(new Data::Entry(m_currColl))); m_isOrphan = true; } // add a message box if multiple items are selected if(m_currEntries.count() > 1) { QStringList names; foreach(Data::EntryPtr entry, m_currEntries) { names += entry->title(); } QString str(i18n("Do you really want to modify these entries?")); QString dontAsk = QStringLiteral("SaveMultipleBooks"); // don't change 'books', invisible anyway int ret = KMessageBox::questionYesNoList(this, str, names, i18n("Modify Multiple Entries"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk); if(ret != KMessageBox::Yes) { m_isWorking = false; return; } } GUI::CursorSaver cs; Data::EntryList oldEntries; Data::FieldList fieldsRequiringValues; // boolean to keep track if any field gets changed bool modified = false; foreach(Data::EntryPtr entry, m_currEntries) { // if the entry is owned, then we're modifying an existing entry, keep a copy of the old one if(entry->isOwned()) { oldEntries.append(Data::EntryPtr(new Data::Entry(*entry))); } foreach(Data::FieldPtr field, m_modifiedFields) { QString key = QString::number(m_currColl->id()) + field->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(widget && widget->isEnabled()) { const QString temp = widget->text(); // ok to set field empty string, just not all of them if(modified == false && entry->field(field) != temp) { modified = true; } entry->setField(field, temp); if(temp.isEmpty()) { const QString prop = field->property(QStringLiteral("required")).toLower(); if(prop == QLatin1String("1") || prop == QLatin1String("true")) { fieldsRequiringValues.append(field); } } } } } if(!fieldsRequiringValues.isEmpty()) { GUI::CursorSaver cs2(Qt::ArrowCursor); QString str = i18n("A value is required for the following fields. Do you want to continue?"); QStringList titles; foreach(Data::FieldPtr it, fieldsRequiringValues) { titles << it->title(); } QString dontAsk = QStringLiteral("SaveWithoutRequired"); int ret = KMessageBox::questionYesNoList(this, str, titles, i18n("Modify Entries"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk); if(ret != KMessageBox::Yes) { m_isWorking = false; return; } } // if something was not empty, signal a save if(modified) { m_isOrphan = false; if(oldEntries.isEmpty()) { Kernel::self()->addEntries(m_currEntries, false); } else { QStringList fieldNames; foreach(Data::FieldPtr field, m_modifiedFields) { fieldNames << field->name(); } Kernel::self()->modifyEntries(oldEntries, m_currEntries, fieldNames); } if(!m_currEntries.isEmpty() && !m_currEntries[0]->title().isEmpty()) { setWindowTitle(i18n("Edit Entry") + QLatin1String(" - ") + m_currEntries[0]->title()); } } m_isWorking = false; slotSetModified(false); // slotHandleNew(); } void EntryEditDialog::clear() { if(m_isWorking) { return; } m_isWorking = true; // clear the widgets foreach(GUI::FieldWidget* widget, m_widgetDict) { widget->setEnabled(true); widget->clear(); widget->insertDefault(); } m_modifiedFields.clear(); setWindowTitle(i18n("Edit Entry")); if(m_isOrphan) { if(m_currEntries.count() > 1) { myWarning() << "is an orphan, but more than one"; } m_isOrphan = false; } m_currEntries.clear(); m_saveButton->setText(i18n("Sa&ve Entry")); m_isWorking = false; slotSetModified(false); } void EntryEditDialog::setContents(Tellico::Data::EntryList entries_) { // this slot might get called if we try to save multiple items, so just return if(m_isWorking) { return; } if(entries_.isEmpty()) { if(queryModified()) { blockSignals(true); slotHandleNew(); blockSignals(false); } return; } // if some entries get selected in one view, then in another, don't reset if(!m_needReset && entries_ == m_currEntries) { return; } m_needReset = false; // first set contents to first item - setContents(entries_.front()); + setEntry(entries_.front()); // something weird...if list count can actually be 1 before the setContents call // and 0 after it. Why is that? It's const! if(entries_.count() < 2) { return; } // multiple entries, so don't set caption setWindowTitle(i18n("Edit Entries")); m_currEntries = entries_; m_isWorking = true; blockSignals(true); foreach(Data::FieldPtr fIt, m_currColl->fields()) { QString key = QString::number(m_currColl->id()) + fIt->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(!widget) { // probably read-only continue; } widget->editMultiple(true); QString value = entries_[0]->field(fIt); for(int i = 1; i < entries_.count(); ++i) { // skip checking the first one if(entries_[i]->field(fIt) != value) { widget->setEnabled(false); break; } } } // end field loop blockSignals(false); m_isWorking = false; m_saveButton->setText(i18n("Sa&ve Entries")); } -void EntryEditDialog::setContents(Tellico::Data::EntryPtr entry_) { +void EntryEditDialog::setEntry(Tellico::Data::EntryPtr entry_) { if(m_isWorking || !queryModified()) { return; } if(!entry_) { myDebug() << "null entry pointer"; slotHandleNew(); return; } // myDebug() << entry_->title(); blockSignals(true); clear(); blockSignals(false); m_isWorking = true; m_currEntries.append(entry_); if(!entry_->title().isEmpty()) { setWindowTitle(i18n("Edit Entry") + QLatin1String(" - ") + entry_->title()); } if(m_currColl != entry_->collection()) { myDebug() << "collections don't match"; m_currColl = entry_->collection(); } foreach(Data::FieldPtr field, m_currColl->fields()) { QString key = QString::number(m_currColl->id()) + field->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(!widget) { // is probably read-only continue; } widget->setText(entry_->field(field)); widget->setEnabled(true); widget->editMultiple(false); } // end field loop if(entry_->isOwned()) { m_saveButton->setText(i18n("Sa&ve Entry")); slotSetModified(false); } else { // saving is necessary for unowned entries slotSetModified(true); } m_isWorking = false; } void EntryEditDialog::removeField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr field_) { if(!field_) { return; } // myDebug() << "name = " << field_->name(); QString key = QString::number(m_currColl->id()) + field_->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(widget) { m_widgetDict.remove(key); // if this is the last field in the category, need to remove the tab page // this function is called after the field has been removed from the collection, // so the category should be gone from the category list if(m_currColl->fieldCategories().indexOf(field_->category()) == -1) { // myDebug() << "last field in the category"; // fragile, widget's parent is the grid, whose parent is the tab page QWidget* w = widget->parentWidget()->parentWidget(); m_tabs->removeTab(m_tabs->indexOf(w)); delete w; // automatically deletes child widget } else { // much of this replicates code in resetLayout() QGridLayout* layout = static_cast(widget->parentWidget()->layout()); delete widget; // automatically removes from layout QVector expands(NCOLS, false); QVector maxWidth(NCOLS, 0); Data::FieldList vec = m_currColl->fieldsByCategory(field_->category()); int count = 0; foreach(Data::FieldPtr field, vec) { GUI::FieldWidget* widget = m_widgetDict.value(QString::number(m_currColl->id()) + field->name()); if(widget) { layout->removeWidget(widget); layout->addWidget(widget, count/NCOLS, count%NCOLS); maxWidth[count%NCOLS] = qMax(maxWidth[count%NCOLS], widget->labelWidth()); if(widget->expands()) { expands[count%NCOLS] = true; } widget->updateGeometry(); ++count; } } // now, the labels in a column should all be the same width count = 0; foreach(Data::FieldPtr field, vec) { GUI::FieldWidget* widget = m_widgetDict.value(QString::number(m_currColl->id()) + field->name()); if(widget) { widget->setLabelWidth(maxWidth[count%NCOLS]); ++count; } } // update stretch factors for columns with a line edit for(int col = 0; col < NCOLS; ++col) { if(expands[col]) { layout->setColumnStretch(col, 1); } } } } } void EntryEditDialog::updateCompletions(Tellico::Data::EntryPtr entry_) { #ifndef NDEBUG if(m_currColl != entry_->collection()) { myDebug() << "inconsistent collection pointers!"; // m_currColl = entry_->collection(); } #endif foreach(Data::FieldPtr f, m_currColl->fields()) { if(f->type() != Data::Field::Line || !f->hasFlag(Data::Field::AllowCompletion)) { continue; } QString key = QString::number(m_currColl->id()) + f->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(!widget) { continue; } if(f->hasFlag(Data::Field::AllowMultiple)) { QStringList items = FieldFormat::splitValue(entry_->field(f)); for(QStringList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { widget->addCompletionObjectItem(*it); } } else { widget->addCompletionObjectItem(entry_->field(f)); } } } void EntryEditDialog::slotSetModified(bool mod_/*=true*/) { m_modified = mod_; m_saveButton->setEnabled(mod_); } bool EntryEditDialog::queryModified() { bool ok = true; // assume that if the dialog is hidden, we shouldn't ask the user to modify changes if(!isVisible()) { m_modified = false; } if(m_modified) { QString str(i18n("The current entry has been modified.\n" "Do you want to enter the changes?")); KGuiItem item = KStandardGuiItem::save(); item.setText(i18n("Save Entry")); int want_save = KMessageBox::warningYesNoCancel(this, str, i18n("Unsaved Changes"), item, KStandardGuiItem::discard()); switch(want_save) { case KMessageBox::Yes: slotHandleSave(); ok = true; break; case KMessageBox::No: m_modified = false; ok = true; break; case KMessageBox::Cancel: ok = false; break; } } return ok; } void EntryEditDialog::addField(Tellico::Data::CollPtr coll_, Tellico::Data::FieldPtr field_) { Q_ASSERT(coll_ == m_currColl); Q_UNUSED(field_); resetLayout(coll_); } // modified fields will always have the same name void EntryEditDialog::modifyField(Tellico::Data::CollPtr coll_, Tellico::Data::FieldPtr oldField_, Tellico::Data::FieldPtr newField_) { // myDebug() << newField_->name(); if(coll_ != m_currColl) { myDebug() << "wrong collection pointer!"; m_currColl = coll_; } // if the field type changed, go ahead and redo the whole layout // also if the category changed for a non-single field, since a new tab must be created if(oldField_->type() != newField_->type() || (oldField_->category() != newField_->category() && !newField_->isSingleCategory())) { bool modified = m_modified; resetLayout(coll_); setContents(m_currEntries); m_modified = modified; return; } QString key = QString::number(coll_->id()) + oldField_->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(widget) { widget->updateField(oldField_, newField_); // need to update label widths if(newField_->title() != oldField_->title()) { int maxWidth = 0; QList childList = widget->parentWidget()->findChildren(); foreach(GUI::FieldWidget* obj, childList) { maxWidth = qMax(maxWidth, obj->labelWidth()); } foreach(GUI::FieldWidget* obj, childList) { obj->setLabelWidth(maxWidth); } } // this is very fragile! // field widgets's parent is the grid, whose parent is the tab page // this is for singleCategory fields if(newField_->category() != oldField_->category()) { int idx = m_tabs->indexOf(widget->parentWidget()->parentWidget()); if(idx > -1) { m_tabs->setTabText(idx, newField_->category()); } } } } void EntryEditDialog::addEntries(Tellico::Data::EntryList entries_) { foreach(Data::EntryPtr entry, entries_) { updateCompletions(entry); } } void EntryEditDialog::modifyEntries(Tellico::Data::EntryList entries_) { bool updateContents = false; foreach(Data::EntryPtr entry, entries_) { updateCompletions(entry); if(!updateContents && m_currEntries.contains(entry)) { updateContents = true; } } if(updateContents) { m_needReset = true; setContents(m_currEntries); } } void EntryEditDialog::fieldValueChanged(Data::FieldPtr field_) { if(!m_modifiedFields.contains(field_)) { m_modifiedFields.append(field_); } slotSetModified(true); } void EntryEditDialog::showEvent(QShowEvent* event_) { QDialog::showEvent(event_); /* I attempted to read and restore window size here, but it didn't work (July 2016) I discovered that I had to put it in a timer. Somewhere, the resize event or something was overriding any size changes I did here. Calling this->resize() would work but windowHandle()->resize() would not (as KWindowConfig::restoreWindowSize uses) */ - QTimer::singleShot(0, this, SLOT(slotUpdateSize())); + QTimer::singleShot(0, this, &EntryEditDialog::slotUpdateSize); } void EntryEditDialog::slotUpdateSize() { KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString)); KWindowConfig::restoreWindowSize(windowHandle(), config); } void EntryEditDialog::hideEvent(QHideEvent* event_) { KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString)); KWindowConfig::saveWindowSize(windowHandle(), config); config.sync(); if(queryModified()) { QDialog::hideEvent(event_); } else { event_->ignore(); } } void EntryEditDialog::closeEvent(QCloseEvent* event_) { // check to see if an entry should be saved before hiding // block signals so the entry view and selection isn't cleared if(queryModified()) { QDialog::closeEvent(event_); } else { event_->ignore(); } } diff --git a/src/entryeditdialog.h b/src/entryeditdialog.h index 4125a461..8bf228c5 100644 --- a/src/entryeditdialog.h +++ b/src/entryeditdialog.h @@ -1,170 +1,170 @@ /*************************************************************************** Copyright (C) 2001-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 TELLICO_ENTRYEDITDIALOG_H #define TELLICO_ENTRYEDITDIALOG_H #include "observer.h" #include "gui/fieldwidget.h" #include #include namespace Tellico { namespace GUI { class TabWidget; } /** * @author Robby Stephenson */ class EntryEditDialog : public QDialog, public Observer { Q_OBJECT // needed for completion object support friend class GUI::FieldWidget; public: EntryEditDialog(QWidget* parent); ~EntryEditDialog(); /** * Checks to see if any data needs to be saved. Returns @p true if it's ok to continue with * saving or closing the widget. * * @return Returns @p true if either the data has not been modified or the user to save or discard the new data. */ bool queryModified(); /** * Deletes and resets the layout of the tabs. * * @param coll A pointer to the collection whose fields should be used for setting up the layout */ void resetLayout(Data::CollPtr coll); /** * Clears all of the input controls in the widget. The pointer to the * current entry is nullified, but not the pointer to the current collection. */ void clear(); virtual void addEntries(Data::EntryList entries) Q_DECL_OVERRIDE; virtual void modifyEntries(Data::EntryList entries) Q_DECL_OVERRIDE; virtual void addField(Data::CollPtr coll, Data::FieldPtr field) Q_DECL_OVERRIDE; /** * Updates a widget when its field has been modified. The category may have changed, completions may have * been added or removed, or what-have-you. * * @param coll A pointer to the parent collection * @param oldField A pointer to the old field, which should have the same name as the new one * @param newField A pointer to the new field */ virtual void modifyField(Data::CollPtr coll, Data::FieldPtr oldField, Data::FieldPtr newField) Q_DECL_OVERRIDE; /** * Removes a field from the editor. * * @param field The field to be removed */ virtual void removeField(Data::CollPtr, Data::FieldPtr field) Q_DECL_OVERRIDE; public Q_SLOTS: /** * Called when the Close button is clicked. It just hides the dialog. */ virtual void slotClose(); /** * Resets the widget, deleting all of its contents */ void slotReset(); /** * Handles clicking the New button. The old entry pointer is destroyed and a * new one is created, but not added to any collection. */ void slotHandleNew(); /** * Handles clicking the Save button. All the values in the entry widgets are * copied into the entry object. @ref signalSaveEntry is made. The widget is cleared, * and the first tab is shown. */ void slotHandleSave(); /** * This slot is called whenever anything is modified. It's public so I can call it * from a @ref FieldEditWidget. */ void slotSetModified(bool modified=true); /** * Sets the contents of the input controls to match the contents of a list of entries. * * @param list A list of the entries. The data in the first one will be inserted in the controls, and * the widgets will be enabled or not, depending on whether the rest of the entries match the first one. */ void setContents(Tellico::Data::EntryList entries); /** * Override the implementation to check whether the entry needs to be modified */ virtual void reject() Q_DECL_OVERRIDE; protected Q_SLOTS: void slotHelp(); private Q_SLOTS: void fieldValueChanged(Tellico::Data::FieldPtr field); void slotUpdateSize(); private: /** * Sets the contents of the input controls to match the contents of a entry. * * @param entry A pointer to the entry * @param highlight An optional string to highlight */ - void setContents(Data::EntryPtr entry); + void setEntry(Data::EntryPtr entry); /** * Updates the completion objects in the edit boxes to include values * contained in a certain entry. * * @param entry A pointer to the entry */ void updateCompletions(Data::EntryPtr entry); virtual void showEvent(QShowEvent* event) Q_DECL_OVERRIDE; virtual void hideEvent(QHideEvent* event) Q_DECL_OVERRIDE; virtual void closeEvent(QCloseEvent* event) Q_DECL_OVERRIDE; Data::CollPtr m_currColl; Data::EntryList m_currEntries; GUI::TabWidget* m_tabs; QHash m_widgetDict; QPushButton* m_newButton; QPushButton* m_saveButton; bool m_modified; Data::FieldList m_modifiedFields; bool m_isOrphan; bool m_isWorking; bool m_needReset; }; } // end namespace #endif diff --git a/src/entrymatchdialog.cpp b/src/entrymatchdialog.cpp index bd622166..03045469 100644 --- a/src/entrymatchdialog.cpp +++ b/src/entrymatchdialog.cpp @@ -1,139 +1,139 @@ /*************************************************************************** Copyright (C) 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 . * * * ***************************************************************************/ #include "entrymatchdialog.h" #include "entryview.h" #include "entry.h" #include "fetch/fetchmanager.h" #include #include #include #include #include #include #include #include #include #include namespace { static const int DIALOG_MIN_WIDTH = 600; } using namespace Tellico; using Tellico::EntryMatchDialog; EntryMatchDialog::EntryMatchDialog(QWidget* parent_, Data::EntryPtr entryToUpdate_, Fetch::Fetcher::Ptr fetcher_, const EntryUpdater::ResultList& matchResults_) : QDialog(parent_) { Q_ASSERT(entryToUpdate_); Q_ASSERT(fetcher_); setModal(true); setWindowTitle(i18n("Select Match")); QVBoxLayout* mainLayout = new QVBoxLayout(this); setLayout(mainLayout); QWidget* mainWidget = new QWidget(this); mainLayout->addWidget(mainWidget); QWidget* hbox = new QWidget(mainWidget); mainLayout->addWidget(hbox); QHBoxLayout* hboxHBoxLayout = new QHBoxLayout(hbox); hboxHBoxLayout->setMargin(0); hboxHBoxLayout->setSpacing(10); QLabel* icon = new QLabel(hbox); hboxHBoxLayout->addWidget(icon); icon->setPixmap(Fetch::Manager::fetcherIcon(fetcher_, KIconLoader::Panel, 48)); icon->setAlignment(Qt::Alignment(Qt::AlignLeft) | Qt::AlignTop); QString s = i18n("%1 returned multiple results which could match %2, " "the entry currently in the collection. Please select the correct match.", fetcher_->source(), entryToUpdate_->title()); KTextEdit* l = new KTextEdit(hbox); hboxHBoxLayout->addWidget(l); l->setHtml(s); l->setReadOnly(true); l->setMaximumHeight(48); l->setFrameStyle(0); QSplitter* split = new QSplitter(Qt::Vertical, mainWidget); mainLayout->addWidget(split); split->setMinimumHeight(400); m_treeWidget = new QTreeWidget(split); m_treeWidget->setAllColumnsShowFocus(true); m_treeWidget->setSortingEnabled(true); m_treeWidget->setHeaderLabels(QStringList() << i18n("Title") << i18n("Description")); - connect(m_treeWidget, SIGNAL(itemSelectionChanged()), SLOT(slotShowEntry())); + connect(m_treeWidget, &QTreeWidget::itemSelectionChanged, this, &EntryMatchDialog::slotShowEntry); foreach(const EntryUpdater::UpdateResult& res, matchResults_) { Data::EntryPtr matchingEntry = res.first->fetchEntry(); QTreeWidgetItem* item = new QTreeWidgetItem(m_treeWidget, QStringList() << matchingEntry->title() << res.first->desc); m_itemResults.insert(item, res); m_itemEntries.insert(item, matchingEntry); } m_entryView = new EntryView(split); // don't bother creating funky gradient images for compact view m_entryView->setUseGradientImages(false); // set the xslt file AFTER setting the gradient image option m_entryView->setXSLTFile(QStringLiteral("Compact.xsl")); m_entryView->addXSLTStringParam("skip-fields", "id,mdate,cdate"); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); mainLayout->addWidget(buttonBox); setMinimumWidth(qMax(minimumWidth(), DIALOG_MIN_WIDTH)); // have the entry view be taller than the tree widget split->setStretchFactor(1, 10); } void EntryMatchDialog::slotShowEntry() { QTreeWidgetItem* item = m_treeWidget->currentItem(); if(!item) { return; } m_entryView->showEntry(m_itemEntries[item]); } Tellico::EntryUpdater::UpdateResult EntryMatchDialog::updateResult() const { QTreeWidgetItem* item = m_treeWidget->currentItem(); if(!item) { return EntryUpdater::UpdateResult(nullptr, false); } return m_itemResults[item]; } diff --git a/src/entrymerger.cpp b/src/entrymerger.cpp index 4244a12d..28af7028 100644 --- a/src/entrymerger.cpp +++ b/src/entrymerger.cpp @@ -1,134 +1,134 @@ /*************************************************************************** Copyright (C) 2007-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 . * * * ***************************************************************************/ #include "entrymerger.h" #include "entry.h" #include "entrycomparison.h" #include "collection.h" #include "tellico_kernel.h" #include "controller.h" #include "progressmanager.h" #include "gui/statusbar.h" #include "tellico_debug.h" #include #include using namespace Tellico; using Tellico::AskUserResolver; using Tellico::EntryMerger; Tellico::MergeConflictResolver::Result AskUserResolver::resolve(Data::EntryPtr entry1, Data::EntryPtr entry2, Data::FieldPtr field, const QString& value1, const QString& value2) { return static_cast(Kernel::self()->askAndMerge(entry1, entry2, field, value1, value2)); } EntryMerger::EntryMerger(Tellico::Data::EntryList entries_, QObject* parent_) : QObject(parent_), m_entriesToCheck(entries_), m_origCount(entries_.count()), m_cancelled(false) , m_resolver(new AskUserResolver) { m_entriesLeft = m_entriesToCheck; Kernel::self()->beginCommandGroup(i18n("Merge Entries")); QString label = i18n("Merging entries..."); ProgressItem& item = ProgressManager::self()->newProgressItem(this, label, true /*canCancel*/); item.setTotalSteps(m_origCount); connect(&item, &Tellico::ProgressItem::signalCancelled, this, &Tellico::EntryMerger::slotCancel); // done if no entries to merge if(m_origCount < 2) { - QTimer::singleShot(500, this, SLOT(slotCleanup())); + QTimer::singleShot(500, this, &EntryMerger::slotCleanup); } else { slotStartNext(); // starts fetching } } EntryMerger::~EntryMerger() { delete m_resolver; } void EntryMerger::slotStartNext() { QString statusMsg = i18n("Total merged/scanned entries: %1/%2", m_entriesToRemove.count(), m_origCount - m_entriesToCheck.count()); StatusBar::self()->setStatus(statusMsg); ProgressManager::self()->setProgress(this, m_origCount - m_entriesToCheck.count()); Data::EntryPtr baseEntry = m_entriesToCheck[0]; for(int i = 1; i < m_entriesToCheck.count(); ++i) { // skip checking against first Data::EntryPtr it = m_entriesToCheck[i]; bool match = cleanMerge(baseEntry, it); if(!match) { int score = baseEntry->collection()->sameEntry(baseEntry, it); match = score >= EntryComparison::ENTRY_GOOD_MATCH; } if(match) { bool merge_ok = Data::Document::mergeEntry(baseEntry, it, m_resolver); if(merge_ok) { m_entriesToRemove.append(it); m_entriesLeft.removeAll(it); } } } m_entriesToCheck.removeAll(baseEntry); if(m_cancelled || m_entriesToCheck.count() < 2) { - QTimer::singleShot(0, this, SLOT(slotCleanup())); + QTimer::singleShot(0, this, &EntryMerger::slotCleanup); } else { - QTimer::singleShot(0, this, SLOT(slotStartNext())); + QTimer::singleShot(0, this, &EntryMerger::slotStartNext); } } void EntryMerger::slotCancel() { m_cancelled = true; } void EntryMerger::slotCleanup() { Kernel::self()->removeEntries(m_entriesToRemove); Controller::self()->slotUpdateSelection(m_entriesLeft); StatusBar::self()->clearStatus(); ProgressManager::self()->setDone(this); Kernel::self()->endCommandGroup(); deleteLater(); } bool EntryMerger::cleanMerge(Tellico::Data::EntryPtr e1, Tellico::Data::EntryPtr e2) const { // figure out if there's a clean merge possible foreach(Data::FieldPtr field, e1->collection()->fields()) { // do not care about id and dates if(field->name() == QLatin1String("id") || field->name() == QLatin1String("cdate") || field->name() == QLatin1String("mdate")) { continue; } QString val1 = e1->field(field); QString val2 = e2->field(field); if(val1 != val2 && !val1.isEmpty() && !val2.isEmpty()) { return false; } } return true; } diff --git a/src/entryupdatejob.cpp b/src/entryupdatejob.cpp index 81f3015e..d6403e84 100644 --- a/src/entryupdatejob.cpp +++ b/src/entryupdatejob.cpp @@ -1,88 +1,88 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "entryupdatejob.h" #include "entrycomparison.h" #include "document.h" #include "entry.h" #include "collection.h" #include "tellico_debug.h" #include using namespace Tellico; using Tellico::EntryUpdateJob; EntryUpdateJob::EntryUpdateJob(QObject* parent_, Data::EntryPtr entry_, Fetch::Fetcher::Ptr fetcher_, Mode mode_) : KJob(parent_), m_entry(entry_), m_fetcher(fetcher_), m_mode(mode_), m_bestMatchScore(-1) { setCapabilities(KJob::Killable); - connect(m_fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SLOT(slotResult(Tellico::Fetch::FetchResult*))); - connect(m_fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotDone())); + connect(m_fetcher.data(), &Fetch::Fetcher::signalResultFound, + this, &EntryUpdateJob::slotResult); + connect(m_fetcher.data(), &Fetch::Fetcher::signalDone, + this, &EntryUpdateJob::slotDone); } void EntryUpdateJob::start() { - QTimer::singleShot(0, this, SLOT(startUpdate())); + QTimer::singleShot(0, this, &EntryUpdateJob::startUpdate); } void EntryUpdateJob::startUpdate() { m_fetcher->startUpdate(m_entry); } void EntryUpdateJob::slotResult(Tellico::Fetch::FetchResult* result_) { if(!result_) { myDebug() << "null result"; return; } Data::EntryPtr entry = result_->fetchEntry(); Q_ASSERT(entry); const int match = m_entry->collection()->sameEntry(m_entry, entry); if(match > m_bestMatchScore) { m_bestMatchScore = match; m_bestMatchEntry = entry; } // if perfect match, go ahead and top if(match > EntryComparison::ENTRY_PERFECT_MATCH) { doKill(); } } void EntryUpdateJob::slotDone() { if(m_bestMatchEntry) { const int matchToBeat = (m_mode == PerfectMatchOnly ? EntryComparison::ENTRY_PERFECT_MATCH : EntryComparison::ENTRY_GOOD_MATCH); if(m_bestMatchScore > matchToBeat) { Data::Document::mergeEntry(m_entry, m_bestMatchEntry); } } emitResult(); } bool EntryUpdateJob::doKill() { m_fetcher->stop(); return true; } diff --git a/src/entryupdater.cpp b/src/entryupdater.cpp index 43cec857..6856512e 100644 --- a/src/entryupdater.cpp +++ b/src/entryupdater.cpp @@ -1,266 +1,265 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "entryupdater.h" #include "entry.h" #include "entrycomparison.h" #include "collection.h" #include "tellico_kernel.h" #include "progressmanager.h" #include "gui/statusbar.h" #include "document.h" #include "fetch/fetchresult.h" #include "entrymatchdialog.h" #include "tellico_debug.h" #include #include #include namespace { static const int CHECK_COLLECTION_IMAGES_STEP_SIZE = 10; } using Tellico::EntryUpdater; // for each entry, we loop over all available fetchers // then we loop over all entries EntryUpdater::EntryUpdater(Tellico::Data::CollPtr coll_, Tellico::Data::EntryList entries_, QObject* parent_) : QObject(parent_) , m_coll(coll_) , m_entriesToUpdate(entries_) , m_cancelled(false) { // for now, we're assuming all entries are same collection type m_fetchers = Fetch::Manager::self()->createUpdateFetchers(m_coll->type()); foreach(Fetch::Fetcher::Ptr fetcher, m_fetchers) { - connect(fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SLOT(slotResult(Tellico::Fetch::FetchResult*))); - connect(fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotDone())); + connect(fetcher.data(), &Fetch::Fetcher::signalResultFound, + this, &EntryUpdater::slotResult); + connect(fetcher.data(), &Fetch::Fetcher::signalDone, + this, &EntryUpdater::slotDone); } init(); } EntryUpdater::EntryUpdater(const QString& source_, Tellico::Data::CollPtr coll_, Tellico::Data::EntryList entries_, QObject* parent_) : QObject(parent_) , m_coll(coll_) , m_entriesToUpdate(entries_) , m_cancelled(false) { // for now, we're assuming all entries are same collection type Fetch::Fetcher::Ptr f = Fetch::Manager::self()->createUpdateFetcher(m_coll->type(), source_); if(f) { m_fetchers.append(f); - connect(f.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SLOT(slotResult(Tellico::Fetch::FetchResult*))); - connect(f.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotDone())); + connect(f.data(), &Fetch::Fetcher::signalResultFound, + this, &EntryUpdater::slotResult); + connect(f.data(), &Fetch::Fetcher::signalDone, + this, &EntryUpdater::slotDone); } init(); } EntryUpdater::~EntryUpdater() { foreach(const UpdateResult& res, m_results) { delete res.first; } m_results.clear(); } void EntryUpdater::init() { m_fetchIndex = 0; m_origEntryCount = m_entriesToUpdate.count(); QString label; if(m_entriesToUpdate.count() == 1) { label = i18n("Updating %1...", m_entriesToUpdate.front()->title()); } else { label = i18n("Updating entries..."); } Kernel::self()->beginCommandGroup(i18n("Update Entries")); ProgressItem& item = ProgressManager::self()->newProgressItem(this, label, true /*canCancel*/); item.setTotalSteps(m_fetchers.count() * m_origEntryCount); connect(&item, &Tellico::ProgressItem::signalCancelled, this, &Tellico::EntryUpdater::slotCancel); // done if no fetchers available if(m_fetchers.isEmpty()) { - QTimer::singleShot(500, this, SLOT(slotCleanup())); + QTimer::singleShot(500, this, &EntryUpdater::slotCleanup); } else { slotStartNext(); // starts fetching } } void EntryUpdater::slotStartNext() { StatusBar::self()->setStatus(i18n("Updating %1...", m_entriesToUpdate.front()->title())); ProgressManager::self()->setProgress(this, m_fetchers.count() * (m_origEntryCount - m_entriesToUpdate.count()) + m_fetchIndex); Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex]; // myDebug() << "starting " << f->source(); f->startUpdate(m_entriesToUpdate.front()); } void EntryUpdater::slotDone() { if(m_cancelled) { - // myLog() << "already cancelled"; - QTimer::singleShot(500, this, SLOT(slotCleanup())); + QTimer::singleShot(500, this, &EntryUpdater::slotCleanup); return; } if(!m_results.isEmpty()) { handleResults(); } m_results.clear(); ++m_fetchIndex; // myDebug() << m_fetchIndex; if(m_fetchIndex == m_fetchers.count()) { m_fetchIndex = 0; // we've gone through the loop for the first entry in the vector // pop it and move on m_entriesToUpdate.removeAll(m_entriesToUpdate.front()); // if there are no more entries, and this is the last fetcher, time to delete if(m_entriesToUpdate.isEmpty()) { - QTimer::singleShot(500, this, SLOT(slotCleanup())); + QTimer::singleShot(500, this, &EntryUpdater::slotCleanup); return; } } qApp->processEvents(); // so the entry updater can clean up a bit - QTimer::singleShot(500, this, SLOT(slotStartNext())); + QTimer::singleShot(500, this, &EntryUpdater::slotStartNext); } void EntryUpdater::slotResult(Tellico::Fetch::FetchResult* result_) { if(!result_ || m_cancelled || !result_->fetcher->isSearching()) { return; } // myDebug() << result_->title << " [" << result_->fetcher->source() << "]"; m_results.append(UpdateResult(result_, m_fetchers[m_fetchIndex]->updateOverwrite())); Data::EntryPtr e = result_->fetchEntry(); if(e && !m_entriesToUpdate.isEmpty()) { m_fetchedEntries.append(e); const int match = m_coll->sameEntry(m_entriesToUpdate.front(), e); if(match > EntryComparison::ENTRY_PERFECT_MATCH) { result_->fetcher->stop(); } } qApp->processEvents(); } void EntryUpdater::slotCancel() { m_cancelled = true; Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex]; if(f) { f->stop(); // ends up calling slotDone(); } else { slotDone(); } } void EntryUpdater::handleResults() { Data::EntryPtr entry = m_entriesToUpdate.front(); int best = 0; ResultList matches; foreach(const UpdateResult& res, m_results) { Data::EntryPtr e = res.first->fetchEntry(); if(!e) { continue; } m_fetchedEntries.append(e); int match = m_coll->sameEntry(entry, e); if(match) { // myDebug() << e->title() << "matches by" << match; } // if the match is GOOD but not PERFECT, keep all of them if(match >= EntryComparison::ENTRY_PERFECT_MATCH) { if(match > best) { best = match; matches.clear(); matches.append(res); } else if(match == best) { matches.append(res); } } else if(match >= EntryComparison::ENTRY_GOOD_MATCH) { best = qMax(best, match); // keep all the results that don't exceed the perfect match matches.append(res); } else if(match > best) { best = match; matches.clear(); matches.append(res); } } if(best < EntryComparison::ENTRY_GOOD_MATCH) { if(best > 0) { myDebug() << "no good match (score > 10), best match =" << best << "(" << matches.count() << "matches)"; } return; } // myDebug() << "best match = " << best << " (" << matches.count() << " matches)"; UpdateResult match(nullptr, true); if(matches.count() == 1) { match = matches.front(); } else if(matches.count() > 1) { match = askUser(matches); } // askUser() could come back with nil if(match.first) { mergeCurrent(match.first->fetchEntry(), match.second); } } Tellico::EntryUpdater::UpdateResult EntryUpdater::askUser(const ResultList& results) { EntryMatchDialog dlg(Kernel::self()->widget(), m_entriesToUpdate.front(), m_fetchers[m_fetchIndex], results); if(dlg.exec() != QDialog::Accepted) { return UpdateResult(nullptr, false); } return dlg.updateResult(); } void EntryUpdater::mergeCurrent(Tellico::Data::EntryPtr entry_, bool overWrite_) { Data::EntryPtr currEntry = m_entriesToUpdate.front(); if(entry_) { m_matchedEntries.append(entry_); Kernel::self()->updateEntry(currEntry, entry_, overWrite_); if(m_entriesToUpdate.count() % CHECK_COLLECTION_IMAGES_STEP_SIZE == 1) { // I don't want to remove any images in the entries that are getting // updated since they'll reference them later and the command isn't // executed until the command history group is finished // so remove pointers to matched entries Data::EntryList nonUpdatedEntries = m_fetchedEntries; foreach(Data::EntryPtr match, m_matchedEntries) { nonUpdatedEntries.removeAll(match); } Data::Document::self()->removeImagesNotInCollection(nonUpdatedEntries, m_matchedEntries); } } } void EntryUpdater::slotCleanup() { - StatusBar::self()->clearStatus(); ProgressManager::self()->setDone(this); + StatusBar::self()->clearStatus(); Kernel::self()->endCommandGroup(); deleteLater(); } diff --git a/src/entryview.cpp b/src/entryview.cpp index f798daf4..198afa97 100644 --- a/src/entryview.cpp +++ b/src/entryview.cpp @@ -1,419 +1,421 @@ /*************************************************************************** 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 . * * * ***************************************************************************/ #include "entryview.h" #include "entry.h" #include "field.h" #include "translators/xslthandler.h" #include "translators/tellicoxmlexporter.h" #include "collection.h" #include "images/imagefactory.h" #include "images/imageinfo.h" #include "tellico_kernel.h" #include "utils/tellico_utils.h" #include "utils/datafileregistry.h" #include "core/filehandler.h" #include "config/tellico_config.h" #include "gui/drophandler.h" #include "document.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include using Tellico::EntryView; using Tellico::EntryViewWidget; EntryViewWidget::EntryViewWidget(EntryView* part, QWidget* parent) : KHTMLView(part, parent) {} // for the life of me, I could not figure out how to call the actual // KHTMLPartBrowserExtension::copy() slot, so this will have to do void EntryViewWidget::copy() { QApplication::clipboard()->setText(part()->selectedText(), QClipboard::Clipboard); } void EntryViewWidget::changeEvent(QEvent* event_) { // this will delete and reread the default colors, assuming they changed if(event_->type() == QEvent::PaletteChange || event_->type() == QEvent::FontChange || event_->type() == QEvent::ApplicationFontChange) { static_cast(part())->resetView(); } KHTMLView::changeEvent(event_); } EntryView::EntryView(QWidget* parent_) : KHTMLPart(new EntryViewWidget(this, parent_), parent_), m_handler(nullptr), m_tempFile(nullptr), m_useGradientImages(true), m_checkCommonFile(true) { setJScriptEnabled(false); setJavaEnabled(false); setMetaRefreshEnabled(false); setPluginsEnabled(false); clear(); // needed for initial layout view()->setAcceptDrops(true); DropHandler* drophandler = new DropHandler(this); view()->installEventFilter(drophandler); - connect(browserExtension(), SIGNAL(openUrlRequestDelayed(const QUrl&, const KParts::OpenUrlArguments&, const KParts::BrowserArguments&)), - SLOT(slotOpenURL(const QUrl&))); + connect(browserExtension(), &KParts::BrowserExtension::openUrlRequestDelayed, + this, &EntryView::slotOpenURL); } EntryView::~EntryView() { delete m_handler; m_handler = nullptr; delete m_tempFile; m_tempFile = nullptr; } void EntryView::clear() { m_entry = nullptr; // just clear the view begin(); if(!m_textToShow.isEmpty()) { write(m_textToShow); } end(); view()->layout(); // I need this because some of the margins and widths may get messed up } void EntryView::showEntries(Tellico::Data::EntryList entries_) { if(!entries_.isEmpty()) { showEntry(entries_.first()); } } void EntryView::showEntry(Tellico::Data::EntryPtr entry_) { if(!entry_) { clear(); return; } m_textToShow.clear(); #if 0 myWarning() << "turn me off!"; m_entry = 0; setXSLTFile(m_xsltFile); #endif if(!m_handler || !m_handler->isValid()) { setXSLTFile(m_xsltFile); } if(!m_handler || !m_handler->isValid()) { myWarning() << "no xslt handler"; return; } m_entry = entry_; // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found // by simply using a relative path in the xslt file QUrl u = QUrl::fromLocalFile(m_xsltFile); begin(u); Export::TellicoXMLExporter exporter(entry_->collection()); exporter.setEntries(Data::EntryList() << entry_); long opt = exporter.options(); // verify images for the view opt |= Export::ExportVerifyImages; // on second thought, don't auto-format everything, just clean it // if(Data::Field::autoFormat()) { // opt = Export::ExportFormatted; // } if(entry_->collection()->type() == Data::Collection::Bibtex) { opt |= Export::ExportClean; } exporter.setOptions(opt); QDomDocument dom = exporter.exportXML(); // myDebug() << dom.toString(); #if 0 myWarning() << "turn me off!"; QFile f1(QLatin1String("/tmp/test.xml")); if(f1.open(QIODevice::WriteOnly)) { QTextStream t(&f1); t << dom.toString(); } f1.close(); #endif QString html = m_handler->applyStylesheet(dom.toString()); // write out image files Data::FieldList fields = entry_->collection()->imageFields(); foreach(Data::FieldPtr field, fields) { QString id = entry_->field(field); if(id.isEmpty()) { continue; } // only write out image if it's not linked only if(!ImageFactory::imageInfo(id).linkOnly) { if(Data::Document::self()->allImagesOnDisk()) { ImageFactory::writeCachedImage(id, ImageFactory::cacheDir()); } else { ImageFactory::writeCachedImage(id, ImageFactory::TempDir); } } } #if 0 myWarning() << "turn me off!"; QFile f2(QLatin1String("/tmp/test.html")); if(f2.open(QIODevice::WriteOnly)) { QTextStream t(&f2); t << html; } f2.close(); #endif // myDebug() << html; write(html); end(); // not need anymore? view()->layout(); // I need this because some of the margins and widths may get messed up } void EntryView::showText(const QString& text_) { m_textToShow = text_; begin(); write(text_); end(); } void EntryView::setXSLTFile(const QString& file_) { if(file_.isEmpty()) { myWarning() << "empty xslt file"; return; } QString oldFile = m_xsltFile; // if starts with slash, then absolute path if(file_.at(0) == QLatin1Char('/')) { m_xsltFile = file_; } else { const QString templateDir = QStringLiteral("entry-templates/"); m_xsltFile = DataFileRegistry::self()->locate(templateDir + file_); if(m_xsltFile.isEmpty()) { if(!file_.isEmpty()) { myWarning() << "can't locate" << file_; } m_xsltFile = DataFileRegistry::self()->locate(templateDir + QLatin1String("Fancy.xsl")); if(m_xsltFile.isEmpty()) { QString str = QStringLiteral(""); str += i18n("Tellico is unable to locate the default entry stylesheet."); str += QLatin1Char(' '); str += i18n("Please check your installation."); str += QLatin1String(""); KMessageBox::error(view(), str); clear(); return; } } } const int type = m_entry ? m_entry->collection()->type() : Kernel::self()->collectionType(); // we need to know if the colors changed from last time, in case // we need to do that ugly hack to reload the cache bool reloadImages = m_useGradientImages; // if m_useGradientImages is false, then we don't even need to check // if there's no handler, there there's _no way_ to check if(m_handler && reloadImages) { // the only two colors that matter for the gradients are the base color // and highlight base color QByteArray oldBase = m_handler->param("bgcolor"); QByteArray oldHigh = m_handler->param("color2"); // remember the string params have apostrophes on either side, so we can start search at pos == 1 reloadImages = oldBase.indexOf(Config::templateBaseColor(type).name().toLatin1(), 1) == -1 || oldHigh.indexOf(Config::templateHighlightedBaseColor(type).name().toLatin1(), 1) == -1; } if(!m_handler || m_xsltFile != oldFile) { delete m_handler; // must read the file name to get proper context m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile)); if(m_checkCommonFile && !m_handler->isValid()) { Tellico::checkCommonXSLFile(); m_checkCommonFile = false; delete m_handler; m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile)); } if(!m_handler->isValid()) { myWarning() << "invalid xslt handler"; clear(); delete m_handler; m_handler = nullptr; return; } } m_handler->addStringParam("font", Config::templateFont(type).family().toLatin1()); m_handler->addStringParam("fontsize", QByteArray().setNum(Config::templateFont(type).pointSize())); m_handler->addStringParam("bgcolor", Config::templateBaseColor(type).name().toLatin1()); m_handler->addStringParam("fgcolor", Config::templateTextColor(type).name().toLatin1()); m_handler->addStringParam("color1", Config::templateHighlightedTextColor(type).name().toLatin1()); m_handler->addStringParam("color2", Config::templateHighlightedBaseColor(type).name().toLatin1()); if(Data::Document::self()->allImagesOnDisk()) { m_handler->addStringParam("imgdir", QFile::encodeName(ImageFactory::imageDir())); } else { m_handler->addStringParam("imgdir", QFile::encodeName(ImageFactory::tempDir())); } m_handler->addStringParam("datadir", QFile::encodeName(Tellico::installationDir())); // if we don't have to reload the images, then just show the entry and we're done if(reloadImages) { // now, have to recreate images and refresh khtml cache resetColors(); } else { showEntry(m_entry); } } void EntryView::slotRefresh() { setXSLTFile(m_xsltFile); showEntry(m_entry); view()->repaint(); } // do some contortions in case the url is relative // need to interpret it relative to document URL instead of xslt file // the current node under the mouse would be the text node inside // the anchor node, so iterate up the parents void EntryView::slotOpenURL(const QUrl& url_) { if(url_.scheme() == QLatin1String("tc")) { // handle this internally emit signalAction(url_); return; } QUrl u = url_; for(DOM::Node node = nodeUnderMouse(); !node.isNull(); node = node.parentNode()) { if(node.nodeType() == DOM::Node::ELEMENT_NODE && static_cast(node).tagName() == "a") { QString href = static_cast(node).getAttribute("href").string(); if(!href.isEmpty()) { // interpret url relative to document url u = Kernel::self()->URL().resolved(QUrl(href)); } break; } } // open the url QDesktopServices::openUrl(u); } void EntryView::slotReloadEntry() { // this slot should only be connected in setXSLTFile() // must disconnect the signal first, otherwise, get an infinite loop - disconnect(SIGNAL(completed())); + void (EntryView::* completed)() = &EntryView::completed; + disconnect(this, completed, this, &EntryView::slotReloadEntry); closeUrl(); // this is needed to stop everything, for some reason view()->setUpdatesEnabled(true); if(m_entry) { showEntry(m_entry); } else { // setXSLTFile() writes some html to clear the image cache // but we don't want to see that, so just clear everything clear(); } delete m_tempFile; m_tempFile = nullptr; } void EntryView::addXSLTStringParam(const QByteArray& name_, const QByteArray& value_) { if(!m_handler) { return; } m_handler->addStringParam(name_, value_); } void EntryView::setXSLTOptions(const Tellico::StyleOptions& opt_) { if(!m_handler) { return; } m_handler->addStringParam("font", opt_.fontFamily.toLatin1()); m_handler->addStringParam("fontsize", QByteArray().setNum(opt_.fontSize)); m_handler->addStringParam("bgcolor", opt_.baseColor.name().toLatin1()); m_handler->addStringParam("fgcolor", opt_.textColor.name().toLatin1()); m_handler->addStringParam("color1", opt_.highlightedTextColor.name().toLatin1()); m_handler->addStringParam("color2", opt_.highlightedBaseColor.name().toLatin1()); m_handler->addStringParam("imgdir", QFile::encodeName(opt_.imgDir)); } void EntryView::resetView() { delete m_handler; m_handler = nullptr; setXSLTFile(m_xsltFile); // this ends up calling resetColors() } void EntryView::resetColors() { // recreate gradients ImageFactory::createStyleImages(m_entry ? m_entry->collection()->type() : Data::Collection::Base); QString dir = m_handler ? QFile::decodeName(m_handler->param("imgdir")) : QString(); if(dir.isEmpty()) { dir = Data::Document::self()->allImagesOnDisk() ? ImageFactory::imageDir() : ImageFactory::tempDir(); } else { // it's a string param, so it has quotes on both sides dir = dir.mid(1); dir.truncate(dir.length()-1); } // this is a rather bad hack to get around the fact that the image cache is not reloaded when // the gradient files are changed on disk. Setting the URLArgs for write() calls doesn't seem to // work. So force a reload with a temp file, then catch the completed signal and repaint QString s = QStringLiteral("") .arg(dir + QLatin1String("gradient_bg.png"), dir + QLatin1String("gradient_header.png")); delete m_tempFile; m_tempFile = new QTemporaryFile(); if(!m_tempFile->open()) { myDebug() << "failed to open temp file"; delete m_tempFile; m_tempFile = nullptr; return; } QTextStream stream(m_tempFile); stream << s; stream.flush(); KParts::OpenUrlArguments args = arguments(); args.setReload(true); // tell the cache to reload images setArguments(args); // don't flicker view()->setUpdatesEnabled(false); openUrl(QUrl::fromLocalFile(m_tempFile->fileName())); - connect(this, SIGNAL(completed()), SLOT(slotReloadEntry())); + void (EntryView::* completed)() = &EntryView::completed; + connect(this, completed, this, &EntryView::slotReloadEntry); } diff --git a/src/exportdialog.cpp b/src/exportdialog.cpp index 287adec3..d1bf76f3 100644 --- a/src/exportdialog.cpp +++ b/src/exportdialog.cpp @@ -1,306 +1,306 @@ /*************************************************************************** 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 . * * * ***************************************************************************/ #include "exportdialog.h" #include "collection.h" #include "core/filehandler.h" #include "controller.h" #include "tellico_debug.h" #include "translators/exporter.h" #include "translators/tellicoxmlexporter.h" #include "translators/tellicozipexporter.h" #include "translators/htmlexporter.h" #include "translators/csvexporter.h" #include "translators/bibtexexporter.h" #include "translators/bibtexmlexporter.h" #include "translators/xsltexporter.h" #include "translators/alexandriaexporter.h" #include "translators/onixexporter.h" #include "translators/gcstarexporter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::ExportDialog; ExportDialog::ExportDialog(Tellico::Export::Format format_, Tellico::Data::CollPtr coll_, QWidget* parent_) : QDialog(parent_), m_format(format_), m_coll(coll_), m_exporter(exporter(format_, coll_)) { setModal(true); setWindowTitle(i18n("Export Options")); QWidget* widget = new QWidget(this); QVBoxLayout* topLayout = new QVBoxLayout(widget); setLayout(topLayout); topLayout->addWidget(widget); QGroupBox* group1 = new QGroupBox(i18n("Formatting"), widget); topLayout->addWidget(group1, 0); QVBoxLayout* vlay = new QVBoxLayout(group1); m_formatFields = new QCheckBox(i18n("Format all fields"), group1); m_formatFields->setChecked(false); m_formatFields->setWhatsThis(i18n("If checked, the values of the fields will be " "automatically formatted according to their format type.")); vlay->addWidget(m_formatFields); m_exportSelected = new QCheckBox(i18n("Export selected entries only"), group1); m_exportSelected->setChecked(false); m_exportSelected->setWhatsThis(i18n("If checked, only the currently selected entries will " "be exported.")); vlay->addWidget(m_exportSelected); m_exportFields = new QCheckBox(i18n("Export visible fields only"), group1); m_exportFields->setChecked(false); m_exportFields->setWhatsThis(i18n("If checked, only the fields currently visible in the view will " "be exported.")); vlay->addWidget(m_exportFields); QGroupBox* group2 = new QGroupBox(i18n("Encoding"), widget); topLayout->addWidget(group2, 0); QVBoxLayout* vlay2 = new QVBoxLayout(group2); m_encodeUTF8 = new QRadioButton(i18n("Encode in Unicode (UTF-8)"), group2); m_encodeUTF8->setChecked(true); m_encodeUTF8->setWhatsThis(i18n("Encode the exported file in Unicode (UTF-8).")); vlay2->addWidget(m_encodeUTF8); QString localStr = i18n("Encode in user locale (%1)", QLatin1String(QTextCodec::codecForLocale()->name())); m_encodeLocale = new QRadioButton(localStr, group2); m_encodeLocale->setWhatsThis(i18n("Encode the exported file in the local encoding.")); vlay2->addWidget(m_encodeLocale); if(QTextCodec::codecForLocale()->name() == "UTF-8") { m_encodeUTF8->setEnabled(false); m_encodeLocale->setChecked(true); } QButtonGroup* bg = new QButtonGroup(widget); bg->addButton(m_encodeUTF8); bg->addButton(m_encodeLocale); QWidget* w = m_exporter->widget(widget); if(w) { w->layout()->setMargin(0); topLayout->addWidget(w, 0); } topLayout->addStretch(); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(okButton, SIGNAL(clicked()), SLOT(slotSaveOptions())); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(okButton, &QAbstractButton::clicked, this, &ExportDialog::slotSaveOptions); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); topLayout->addWidget(buttonBox); readOptions(); if(format_ == Export::Alexandria) { // no encoding options enabled group2->setEnabled(false); } } ExportDialog::~ExportDialog() { delete m_exporter; m_exporter = nullptr; } QString ExportDialog::fileFilter() { return m_exporter ? m_exporter->fileFilter() : QString(); } void ExportDialog::readOptions() { KConfigGroup config(KSharedConfig::openConfig(), "ExportOptions"); bool format = config.readEntry("FormatFields", false); m_formatFields->setChecked(format); bool selected = config.readEntry("ExportSelectedOnly", false); m_exportSelected->setChecked(selected); bool encode = config.readEntry("EncodeUTF8", true); if(encode && m_encodeUTF8->isEnabled()) { m_encodeUTF8->setChecked(true); } else { m_encodeLocale->setChecked(true); } } void ExportDialog::slotSaveOptions() { KSharedConfigPtr config = KSharedConfig::openConfig(); // each exporter sets its own group m_exporter->saveOptions(config); KConfigGroup configGroup(config, "ExportOptions"); configGroup.writeEntry("FormatFields", m_formatFields->isChecked()); configGroup.writeEntry("ExportSelectedOnly", m_exportSelected->isChecked()); configGroup.writeEntry("EncodeUTF8", m_encodeUTF8->isChecked()); } // static Tellico::Export::Exporter* ExportDialog::exporter(Tellico::Export::Format format_, Data::CollPtr coll_) { Export::Exporter* exporter = nullptr; switch(format_) { case Export::TellicoXML: exporter = new Export::TellicoXMLExporter(coll_); break; case Export::TellicoZip: exporter = new Export::TellicoZipExporter(coll_); break; case Export::HTML: { Export::HTMLExporter* htmlExp = new Export::HTMLExporter(coll_); htmlExp->setGroupBy(Controller::self()->expandedGroupBy()); htmlExp->setSortTitles(Controller::self()->sortTitles()); htmlExp->setColumns(Controller::self()->visibleColumns()); exporter = htmlExp; } break; case Export::CSV: exporter = new Export::CSVExporter(coll_); break; case Export::Bibtex: exporter = new Export::BibtexExporter(coll_); break; case Export::Bibtexml: exporter = new Export::BibtexmlExporter(coll_); break; case Export::XSLT: exporter = new Export::XSLTExporter(coll_); break; case Export::Alexandria: exporter = new Export::AlexandriaExporter(coll_); break; case Export::ONIX: exporter = new Export::ONIXExporter(coll_); break; case Export::GCstar: exporter = new Export::GCstarExporter(coll_); break; default: myDebug() << "not implemented!"; break; } if(exporter) { exporter->readOptions(KSharedConfig::openConfig()); } return exporter; } bool ExportDialog::exportURL(const QUrl& url_/*=QUrl()*/) const { if(!m_exporter) { return false; } // exporter might need to know final URL, say for writing images or something m_exporter->setURL(url_); if(m_exportSelected->isChecked()) { m_exporter->setEntries(Controller::self()->selectedEntries()); } else { m_exporter->setEntries(m_coll->entries()); } if(m_exportFields->isChecked()) { Data::FieldList fields; foreach(const QString& title, Controller::self()->visibleColumns()) { Data::FieldPtr field = m_coll->fieldByTitle(title); if(field) { fields << field; } } m_exporter->setFields(fields); } else { m_exporter->setFields(m_coll->fields()); } long opt = Export::ExportImages | Export::ExportComplete | Export::ExportProgress; // for now, always export images if(m_formatFields->isChecked()) { opt |= Export::ExportFormatted; } if(m_encodeUTF8->isChecked()) { opt |= Export::ExportUTF8; } // since we already asked about overwriting the file, force the save opt |= Export::ExportForce; m_exporter->setOptions(opt); return m_exporter->exec(); } // static // alexandria is exported to known directory // all others are files Tellico::Export::Target ExportDialog::exportTarget(Tellico::Export::Format format_) { switch(format_) { case Export::Alexandria: return Export::None; default: return Export::File; } } // static bool ExportDialog::exportCollection(Data::CollPtr coll_, Data::EntryList entries_, Export::Format format_, const QUrl& url_) { QScopedPointer exp(exporter(format_, coll_)); exp->setURL(url_); exp->setEntries(entries_); KConfigGroup config(KSharedConfig::openConfig(), "ExportOptions"); long options = 0; if(config.readEntry("FormatFields", false)) { options |= Export::ExportFormatted; } if(config.readEntry("EncodeUTF8", true)) { options |= Export::ExportUTF8; } exp->setOptions(options | Export::ExportForce); return exp->exec(); } diff --git a/src/fetch/allocinefetcher.cpp b/src/fetch/allocinefetcher.cpp index 5762b2f8..4ddc3047 100644 --- a/src/fetch/allocinefetcher.cpp +++ b/src/fetch/allocinefetcher.cpp @@ -1,502 +1,503 @@ /*************************************************************************** Copyright (C) 2012-2013 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 // for TELLICO_VERSION #include "allocinefetcher.h" #include "../collections/videocollection.h" #include "../images/imagefactory.h" #include "../entry.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* ALLOCINE_API_KEY = "100043982026"; static const char* ALLOCINE_API_URL = "http://api.allocine.fr/rest/v3/"; static const char* ALLOCINE_PARTNER_KEY = "29d185d98c984a359e6e6f26a0474269"; } using namespace Tellico; using Tellico::Fetch::AbstractAllocineFetcher; using Tellico::Fetch::AllocineFetcher; AbstractAllocineFetcher::AbstractAllocineFetcher(QObject* parent_, const QString& baseUrl_) : Fetcher(parent_) , m_started(false) , m_apiKey(QLatin1String(ALLOCINE_API_KEY)) , m_baseUrl(baseUrl_) , m_numCast(10) { Q_ASSERT(!m_baseUrl.isEmpty()); } AbstractAllocineFetcher::~AbstractAllocineFetcher() { } bool AbstractAllocineFetcher::canSearch(FetchKey k) const { return k == Keyword; } bool AbstractAllocineFetcher::canFetch(int type) const { return type == Data::Collection::Video; } void AbstractAllocineFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key", ALLOCINE_API_KEY); if(!k.isEmpty()) { m_apiKey = k; } m_numCast = config_.readEntry("Max Cast", 10); } void AbstractAllocineFetcher::search() { m_started = true; QUrl u(m_baseUrl); u = u.adjusted(QUrl::StripTrailingSlash); u.setPath(u.path() + QLatin1Char('/') + QStringLiteral("search")); // myDebug() << u; // the order of the parameters appears to matter QList > params; params.append(qMakePair(QStringLiteral("partner"), m_apiKey)); // I can't figure out how to encode accent marks, but they don't // seem to be necessary QString q = removeAccents(request().value); // should I just remove all non alphabetical characters? // see https://bugs.kde.org/show_bug.cgi?id=337432 q.remove(QRegExp(QStringLiteral("[,:!?;\\(\\)]"))); q.replace(QLatin1Char('\''), QLatin1Char('+')); q.replace(QLatin1Char(' '), QLatin1Char('+')); switch(request().key) { case Keyword: params.append(qMakePair(QStringLiteral("q"), q)); break; default: myWarning() << "key not recognized: " << request().key; return; } params.append(qMakePair(QStringLiteral("format"), QStringLiteral("json"))); params.append(qMakePair(QStringLiteral("filter"), QStringLiteral("movie"))); const QString sed = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd")); params.append(qMakePair(QStringLiteral("sed"), sed)); const QByteArray sig = calculateSignature(params); QUrlQuery query; query.setQueryItems(params); query.addQueryItem(QStringLiteral("sig"), QLatin1String(sig)); u.setQuery(query); // myDebug() << u; m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); // 10/8/17: UserAgent appears necessary to receive data m_job->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") .arg(QStringLiteral(TELLICO_VERSION))); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &AbstractAllocineFetcher::slotComplete); } void AbstractAllocineFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); } m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr AbstractAllocineFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } QString code = entry->field(QStringLiteral("allocine-code")); if(code.isEmpty()) { // could mean we already updated the entry myDebug() << "no allocine release found"; return entry; } QUrl u(m_baseUrl); u = u.adjusted(QUrl::StripTrailingSlash); u.setPath(u.path() + QLatin1Char('/') + QStringLiteral("movie")); // the order of the parameters appears to matter QList > params; params.append(qMakePair(QStringLiteral("partner"), m_apiKey)); params.append(qMakePair(QStringLiteral("code"), code)); params.append(qMakePair(QStringLiteral("profile"), QStringLiteral("large"))); params.append(qMakePair(QStringLiteral("filter"), QStringLiteral("movie"))); params.append(qMakePair(QStringLiteral("format"), QStringLiteral("json"))); const QString sed = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd")); params.append(qMakePair(QStringLiteral("sed"), sed)); const QByteArray sig = calculateSignature(params); QUrlQuery query; query.setQueryItems(params); query.addQueryItem(QStringLiteral("sig"), QLatin1String(sig)); u.setQuery(query); // myDebug() << "url: " << u; // 10/8/17: UserAgent appears necessary to receive data // QByteArray data = FileHandler::readDataFile(u, true); KIO::StoredTransferJob* dataJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); dataJob->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") .arg(QStringLiteral(TELLICO_VERSION))); if(!dataJob->exec()) { myDebug() << "Failed to load" << u; return entry; } const QByteArray data = dataJob->data(); #if 0 myWarning() << "Remove debug2 from allocinefetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test2.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); QVariantMap result = doc.object().toVariantMap().value(QStringLiteral("movie")).toMap(); if(error.error != QJsonParseError::NoError) { myDebug() << "Bad JSON results"; #if 0 myWarning() << "Remove debug3 from allocinefetcher.cpp"; QFile f2(QString::fromLatin1("/tmp/test3.json")); if(f2.open(QIODevice::WriteOnly)) { QTextStream t(&f2); t.setCodec("UTF-8"); t << data; } f2.close(); #endif return entry; } populateEntry(entry, result); // image might still be a URL const QString image_id = entry->field(QStringLiteral("cover")); if(image_id.contains(QLatin1Char('/'))) { const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } // empty image ID is ok entry->setField(QStringLiteral("cover"), id); } // don't want to include id entry->collection()->removeField(QStringLiteral("allocine-code")); QStringList castRows = FieldFormat::splitTable(entry->field(QStringLiteral("cast"))); while(castRows.count() > m_numCast) { castRows.removeLast(); } entry->setField(QStringLiteral("cast"), castRows.join(FieldFormat::rowDelimiterString())); return entry; } void AbstractAllocineFetcher::slotComplete(KJob*) { if(m_job->error()) { myDebug() << "Error:" << m_job->errorString(); m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from allocinefetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap result = doc.object().toVariantMap().value(QStringLiteral("feed")).toMap(); // myDebug() << "total:" << result.value(QLatin1String("totalResults")); QVariantList resultList = result.value(QStringLiteral("movie")).toList(); if(resultList.isEmpty()) { myDebug() << "no results"; stop(); return; } foreach(const QVariant& result, resultList) { // myDebug() << "found result:" << result; //create a new collection for every result since we end up removing the allocine code field // when fetchEntryHook is called. See bug 338389 Data::EntryPtr entry(new Data::Entry(createCollection())); populateEntry(entry, result.toMap()); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } m_hasMoreResults = false; stop(); } Tellico::Data::CollPtr AbstractAllocineFetcher::createCollection() const { Data::CollPtr coll(new Data::VideoCollection(true)); // always add the allocine release code for fetchEntryHook Data::FieldPtr field(new Data::Field(QStringLiteral("allocine-code"), QStringLiteral("Allocine Code"), Data::Field::Number)); field->setCategory(i18n("General")); coll->addField(field); // add new fields if(optionalFields().contains(QStringLiteral("allocine"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("allocine"), i18n("Allocine Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } if(optionalFields().contains(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } return coll; } void AbstractAllocineFetcher::populateEntry(Data::EntryPtr entry, const QVariantMap& resultMap) { if(entry->collection()->hasField(QStringLiteral("allocine-code"))) { entry->setField(QStringLiteral("allocine-code"), mapValue(resultMap, "code")); } entry->setField(QStringLiteral("title"), mapValue(resultMap, "title")); if(optionalFields().contains(QStringLiteral("origtitle"))) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap, "originalTitle")); } if(entry->title().isEmpty()) { entry->setField(QStringLiteral("title"), mapValue(resultMap, "originalTitle")); } entry->setField(QStringLiteral("year"), mapValue(resultMap, "productionYear")); entry->setField(QStringLiteral("plot"), mapValue(resultMap, "synopsis")); const int runTime = mapValue(resultMap, "runtime").toInt(); entry->setField(QStringLiteral("running-time"), QString::number(runTime/60)); const QVariantList castList = resultMap.value(QStringLiteral("castMember")).toList(); QStringList actors, directors, producers, composers; foreach(const QVariant& castVariant, castList) { const QVariantMap castMap = castVariant.toMap(); const int code = mapValue(castMap, "activity", "code").toInt(); switch(code) { case 8001: actors << (mapValue(castMap, "person", "name") + FieldFormat::columnDelimiterString() + mapValue(castMap, "role")); break; case 8002: directors << mapValue(castMap, "person", "name"); break; case 8029: producers << mapValue(castMap, "person", "name"); break; case 8003: composers << mapValue(castMap, "person", "name"); break; } } entry->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString())); entry->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString())); entry->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); entry->setField(QStringLiteral("composer"), composers.join(FieldFormat::delimiterString())); const QVariantMap releaseMap = resultMap.value(QStringLiteral("release")).toMap(); entry->setField(QStringLiteral("studio"), mapValue(releaseMap, "distributor", "name")); QStringList genres; foreach(const QVariant& variant, resultMap.value(QLatin1String("genre")).toList()) { genres << i18n(mapValue(variant.toMap(), "$").toUtf8().constData()); } entry->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString())); QStringList nats; foreach(const QVariant& variant, resultMap.value(QLatin1String("nationality")).toList()) { nats << mapValue(variant.toMap(), "$"); } entry->setField(QStringLiteral("nationality"), nats.join(FieldFormat::delimiterString())); QStringList langs; foreach(const QVariant& variant, resultMap.value(QLatin1String("language")).toList()) { langs << mapValue(variant.toMap(), "$"); } entry->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); const QVariantMap colorMap = resultMap.value(QLatin1String("color")).toMap(); if(colorMap.value(QStringLiteral("code")) == QLatin1String("12001")) { entry->setField(QStringLiteral("color"), i18n("Color")); } entry->setField(QStringLiteral("cover"), mapValue(resultMap, "poster", "href")); if(optionalFields().contains(QStringLiteral("allocine"))) { entry->setField(QStringLiteral("allocine"), mapValue(resultMap, "link", "href")); } } Tellico::Fetch::FetchRequest AbstractAllocineFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Keyword, title); } return FetchRequest(); } AbstractAllocineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AbstractAllocineFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* label = new QLabel(i18n("&Maximum cast: "), optionsWidget()); l->addWidget(label, ++row, 0); m_numCast = new QSpinBox(optionsWidget()); m_numCast->setMaximum(99); m_numCast->setMinimum(0); m_numCast->setValue(10); - connect(m_numCast, SIGNAL(valueChanged(QString)), SLOT(slotSetModified())); + void (QSpinBox::* valueChanged)(const QString&) = &QSpinBox::valueChanged; + connect(m_numCast, valueChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_numCast, row, 1); QString w = i18n("The list of cast members may include many people. Set the maximum number returned from the search."); label->setWhatsThis(w); m_numCast->setWhatsThis(w); label->setBuddy(m_numCast); l->setRowStretch(++row, 10); m_numCast->setValue(fetcher_ ? fetcher_->m_numCast : 10); } void AbstractAllocineFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { config_.writeEntry("Max Cast", m_numCast->value()); } QByteArray AbstractAllocineFetcher::calculateSignature(const QList >& params_) { typedef QPair StringPair; QByteArray queryString; foreach(const StringPair& pair, params_) { queryString.append(pair.first.toUtf8().toPercentEncoding("+")); queryString.append('='); queryString.append(pair.second.toUtf8().toPercentEncoding("+")); queryString.append('&'); } // remove final '&' queryString.chop(1); const QByteArray toSign = ALLOCINE_PARTNER_KEY + queryString; const QByteArray hash = QCryptographicHash::hash(toSign, QCryptographicHash::Sha1); QByteArray sig = hash.toBase64(); return sig; } /**********************************************************************************************/ AllocineFetcher::AllocineFetcher(QObject* parent_) : AbstractAllocineFetcher(parent_, QLatin1String(ALLOCINE_API_URL)) { } QString AllocineFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } Tellico::Fetch::ConfigWidget* AllocineFetcher::configWidget(QWidget* parent_) const { return new AllocineFetcher::ConfigWidget(parent_, this); } QString AllocineFetcher::defaultName() { return QStringLiteral("AlloCiné.fr"); } QString AllocineFetcher::defaultIcon() { return favIcon("http://www.allocine.fr"); } Tellico::StringHash AllocineFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("origtitle")] = i18n("Original Title"); hash[QStringLiteral("allocine")] = i18n("Allocine Link"); return hash; } AllocineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AbstractAllocineFetcher* fetcher_) : AbstractAllocineFetcher::ConfigWidget(parent_, fetcher_) { // now add additional fields widget addFieldsWidget(AllocineFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString AllocineFetcher::ConfigWidget::preferredName() const { return AllocineFetcher::defaultName(); } diff --git a/src/fetch/amazonfetcher.cpp b/src/fetch/amazonfetcher.cpp index aac403bc..4995933c 100644 --- a/src/fetch/amazonfetcher.cpp +++ b/src/fetch/amazonfetcher.cpp @@ -1,1087 +1,1089 @@ /*************************************************************************** Copyright (C) 2004-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 . * * * ***************************************************************************/ #include #include "amazonfetcher.h" #include "amazonrequest.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../collection.h" #include "../entry.h" #include "../field.h" #include "../fieldformat.h" #include "../utils/string_utils.h" #include "../utils/isbnvalidator.h" #include "../utils/datafileregistry.h" #include "../gui/combobox.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int AMAZON_RETURNS_PER_REQUEST = 10; static const int AMAZON_MAX_RETURNS_TOTAL = 20; static const char* AMAZON_ASSOC_TOKEN = "tellico-20"; // need to have these in the translation file static const char* linkText = I18N_NOOP("Amazon Link"); } using namespace Tellico; using Tellico::Fetch::AmazonFetcher; // static const AmazonFetcher::SiteData& AmazonFetcher::siteData(int site_) { Q_ASSERT(site_ >= 0); Q_ASSERT(site_ < 15); static SiteData dataVector[14] = { { i18n("Amazon (US)"), QUrl(QLatin1String("http://webservices.amazon.com/onca/xml")), QLatin1String("us"), i18n("United States") }, { i18n("Amazon (UK)"), QUrl(QLatin1String("http://webservices.amazon.co.uk/onca/xml")), QLatin1String("gb"), i18n("United Kingdom") }, { i18n("Amazon (Germany)"), QUrl(QLatin1String("http://webservices.amazon.de/onca/xml")), QLatin1String("de"), i18n("Germany") }, { i18n("Amazon (Japan)"), QUrl(QLatin1String("http://webservices.amazon.co.jp/onca/xml")), QLatin1String("jp"), i18n("Japan") }, { i18n("Amazon (France)"), QUrl(QLatin1String("http://webservices.amazon.fr/onca/xml")), QLatin1String("fr"), i18n("France") }, { i18n("Amazon (Canada)"), QUrl(QLatin1String("http://webservices.amazon.ca/onca/xml")), QLatin1String("ca"), i18n("Canada") }, { i18n("Amazon (China)"), QUrl(QLatin1String("http://webservices.amazon.cn/onca/xml")), QLatin1String("ch"), i18n("China") }, { i18n("Amazon (Spain)"), QUrl(QLatin1String("http://webservices.amazon.es/onca/xml")), QLatin1String("es"), i18n("Spain") }, { i18n("Amazon (Italy)"), QUrl(QLatin1String("http://webservices.amazon.it/onca/xml")), QLatin1String("it"), i18n("Italy") }, { i18n("Amazon (Brazil)"), QUrl(QLatin1String("http://webservices.amazon.com.br/onca/xml")), QLatin1String("br"), i18n("Brazil") }, { i18n("Amazon (Australia)"), QUrl(QLatin1String("http://webservices.amazon.com.au/onca/xml")), QLatin1String("au"), i18n("Australia") }, { i18n("Amazon (India)"), QUrl(QLatin1String("http://webservices.amazon.in/onca/xml")), QLatin1String("in"), i18n("India") }, { i18n("Amazon (Mexico)"), QUrl(QLatin1String("http://webservices.amazon.com.mx/onca/xml")), QLatin1String("mx"), i18n("Mexico") }, { i18n("Amazon (Turkey)"), QUrl(QLatin1String("http://webservices.amazon.com.tr/onca/xml")), QLatin1String("tr"), i18n("Turkey") } }; return dataVector[qBound(0, site_, static_cast(sizeof(dataVector)/sizeof(SiteData)))]; } AmazonFetcher::AmazonFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_site(Unknown), m_imageSize(MediumImage), m_assoc(QLatin1String(AMAZON_ASSOC_TOKEN)), m_addLinkField(true), m_limit(AMAZON_MAX_RETURNS_TOTAL), m_countOffset(0), m_page(1), m_total(-1), m_numResults(0), m_job(nullptr), m_started(false) { (void)linkText; // just to shut up the compiler } AmazonFetcher::~AmazonFetcher() { delete m_xsltHandler; m_xsltHandler = nullptr; } QString AmazonFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString AmazonFetcher::attribution() const { return i18n("This data is licensed under specific terms.", QLatin1String("https://affiliate-program.amazon.com/gp/advertising/api/detail/agreement.html")); } bool AmazonFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex || type == Data::Collection::Album || type == Data::Collection::Video || type == Data::Collection::Game || type == Data::Collection::BoardGame; } bool AmazonFetcher::canSearch(FetchKey k) const { // no UPC in Canada return k == Title || k == Person || k == ISBN || (k == UPC && m_site != CA) || k == Keyword; } void AmazonFetcher::readConfigHook(const KConfigGroup& config_) { const int site = config_.readEntry("Site", int(Unknown)); Q_ASSERT(site != Unknown); m_site = static_cast(site); if(m_name.isEmpty()) { m_name = siteData(m_site).title; } QString s = config_.readEntry("AccessKey"); if(!s.isEmpty()) { m_access = s; } else { myWarning() << "No Amazon access key"; } s = config_.readEntry("AssocToken"); if(!s.isEmpty()) { m_assoc = s; } s = config_.readEntry("SecretKey"); if(!s.isEmpty()) { m_amazonKey = s.toUtf8(); } else { myWarning() << "No Amazon secret key"; } int imageSize = config_.readEntry("Image Size", -1); if(imageSize > -1) { m_imageSize = static_cast(imageSize); } } void AmazonFetcher::search() { m_started = true; m_page = 1; m_total = -1; m_countOffset = 0; m_numResults = 0; doSearch(); } void AmazonFetcher::continueSearch() { m_started = true; m_limit += AMAZON_MAX_RETURNS_TOTAL; doSearch(); } void AmazonFetcher::doSearch() { // calling secretKey() ensures that we try to read it first if(secretKey().isEmpty() || m_access.isEmpty()) { if(m_access.isEmpty()) { myWarning() << "No Amazon access key"; } // this message is split in two since the first half is reused later message(i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key.") + QLatin1Char(' ') + i18n("Those values must be entered in the data source settings."), MessageHandler::Error); stop(); return; } // myDebug() << "value = " << request().value; // myDebug() << "getting page " << m_page; QMap params; params.insert(QStringLiteral("Service"), QStringLiteral("AWSECommerceService")); params.insert(QStringLiteral("AssociateTag"), m_assoc); params.insert(QStringLiteral("AWSAccessKeyId"), m_access); params.insert(QStringLiteral("Operation"), QStringLiteral("ItemSearch")); params.insert(QStringLiteral("ResponseGroup"), QStringLiteral("Large")); params.insert(QStringLiteral("ItemPage"), QString::number(m_page)); // this should match the namespace in amazon2tellico.xsl params.insert(QStringLiteral("Version"), QStringLiteral("2011-08-01")); const int type = collectionType(); switch(type) { case Data::Collection::Book: case Data::Collection::ComicBook: case Data::Collection::Bibtex: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Books")); params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Album: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Music")); break; case Data::Collection::Video: // CA and JP appear to have a bug where Video only returns VHS or Music results // DVD will return DVD, Blu-ray, etc. so just ignore VHS for those users if(m_site == CA || m_site == JP || m_site == IT || m_site == ES) { params.insert(QStringLiteral("SearchIndex"), QStringLiteral("DVD")); } else { params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Video")); } params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Game: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("VideoGames")); break; case Data::Collection::BoardGame: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Toys")); params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Coin: case Data::Collection::Stamp: case Data::Collection::Wine: case Data::Collection::Base: case Data::Collection::Card: myDebug() << "can't fetch this type:" << collectionType(); stop(); return; } QString value = request().value; switch(request().key) { case Title: params.insert(QStringLiteral("Title"), value); break; case Person: if(type == Data::Collection::Video) { params.insert(QStringLiteral("Actor"), value); params.insert(QStringLiteral("Director"), value); } else if(type == Data::Collection::Album) { params.insert(QStringLiteral("Artist"), value); } else if(type == Data::Collection::Game) { params.insert(QStringLiteral("Manufacturer"), value); } else { // books and bibtex QString s = QStringLiteral("author:%1 or publisher:%2").arg(value, value); // params.insert(QLatin1String("Author"), value, mib); // params.insert(QLatin1String("Publisher"), value, mib); params.insert(QStringLiteral("Power"), s); } break; case ISBN: { params.insert(QStringLiteral("Operation"), QStringLiteral("ItemLookup")); QString cleanValue = value; cleanValue.remove(QLatin1Char('-')); // ISBN only get digits or 'X' QStringList isbns = FieldFormat::splitValue(cleanValue); // Amazon isbn13 search is still very flaky, so if possible, we're going to convert // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an // isbn13 search bool isbn13 = false; for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) { if((*it).startsWith(QLatin1String("979"))) { if(m_site == JP) { // never works for JP myWarning() << "ISBN-13 searching not implemented for Japan"; it = isbns.erase(it); continue; } isbn13 = true; break; } ++it; } // if we want isbn10, then convert all if(!isbn13) { for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { if((*it).length() > 12) { (*it) = ISBNValidator::isbn10(*it); (*it).remove(QLatin1Char('-')); } } // the default search is by ASIN, which prohibits SearchIndex params.remove(QStringLiteral("SearchIndex")); } // limit to first 10 while(isbns.size() > 10) { isbns.pop_back(); } params.insert(QStringLiteral("ItemId"), isbns.join(QLatin1String(","))); if(isbn13) { params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); } } break; case UPC: { QString cleanValue = value; cleanValue.remove(QLatin1Char('-')); // for EAN values, add 0 to beginning if not 13 characters // in order to assume US country code from UPC value QStringList values; foreach(const QString& splitValue, cleanValue.split(FieldFormat::delimiterString())) { QString tmpValue = splitValue; if(m_site != US && tmpValue.length() == 12) { tmpValue.prepend(QLatin1Char('0')); } values << tmpValue; // limit to first 10 values if(values.length() >= 10) { break; } } params.insert(QStringLiteral("Operation"), QStringLiteral("ItemLookup")); // US allows UPC, all others are EAN if(m_site == US) { params.insert(QStringLiteral("IdType"), QStringLiteral("UPC")); } else { params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); } params.insert(QStringLiteral("ItemId"), values.join(QLatin1String(","))); } break; case Keyword: params.insert(QStringLiteral("Keywords"), value); break; case Raw: { QString key = value.section(QLatin1Char('='), 0, 0).trimmed(); QString str = value.section(QLatin1Char('='), 1).trimmed(); params.insert(key, str); } break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } AmazonRequest request(siteData(m_site).url, m_amazonKey); QUrl newUrl = request.signedRequest(params); // myDebug() << newUrl; m_job = KIO::storedGet(newUrl, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &AmazonFetcher::slotComplete); } void AmazonFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void AmazonFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from amazonfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test%1.xml").arg(m_page)); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QStringList errors; if(m_total == -1) { QDomDocument dom; if(!dom.setContent(data, false)) { myWarning() << "server did not return valid XML."; stop(); return; } // check for ItemSearchErrorResponse if(dom.documentElement().tagName() == QLatin1String("ItemSearchErrorResponse")) { QDomNode n = dom.documentElement().namedItem(QStringLiteral("Error")).namedItem(QStringLiteral("Message")); if(!n.isNull()) { message(n.toElement().text(), MessageHandler::Error); stop(); return; } } // find TotalResults element // it's in the first level under the root element //ItemSearchResponse/Items/TotalResults QDomNode n = dom.documentElement().namedItem(QStringLiteral("Items")) .namedItem(QStringLiteral("TotalResults")); QDomElement e = n.toElement(); if(!e.isNull()) { m_total = e.text().toInt(); } n = dom.documentElement().namedItem(QStringLiteral("Items")) .namedItem(QStringLiteral("Request")) .namedItem(QStringLiteral("Errors")); e = n.toElement(); if(!e.isNull()) { QDomNodeList nodes = e.elementsByTagName(QStringLiteral("Error")); for(int i = 0; i < nodes.count(); ++i) { e = nodes.item(i).toElement().namedItem(QStringLiteral("Code")).toElement(); if(!e.isNull() && e.text() == QLatin1String("AWS.ECommerceService.NoExactMatches")) { // no exact match, not a real error, so skip continue; } // for some reason, Amazon will return an error simply when a valid ISBN is not found // I really want to ignore that, so check the IsValid element in the Request element QDomNode isValidNode = n.parentNode().namedItem(QStringLiteral("IsValid")); if(request().key == ISBN && isValidNode.toElement().text().toLower() == QLatin1String("true")) { continue; } e = nodes.item(i).toElement().namedItem(QStringLiteral("Message")).toElement(); if(!e.isNull()) { errors << e.text(); } } } } if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return; } } // QRegExp stripHTML(QLatin1String("<.*>"), true); // stripHTML.setMinimal(true); // assume amazon is always utf-8 QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size())); Import::TellicoImporter imp(str); // be quiet when loading images imp.setOptions(imp.options() ^ Import::ImportShowImageErrors); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no collection pointer"; stop(); return; } if(!m_addLinkField) { // remove amazon field if it's not to be added coll->removeField(QStringLiteral("amazon")); } Data::EntryList entries = coll->entries(); if(entries.isEmpty() && !errors.isEmpty()) { for(QStringList::ConstIterator it = errors.constBegin(); it != errors.constEnd(); ++it) { myDebug() << "AmazonFetcher::" << *it; } message(errors[0], MessageHandler::Error); stop(); return; } int count = -1; foreach(Data::EntryPtr entry, entries) { ++count; if(m_numResults >= m_limit) { break; } if(count < m_countOffset) { continue; } if(!m_started) { // might get aborted break; } // special case book author // amazon is really bad about not putting spaces after periods if(coll->type() == Data::Collection::Book) { QRegExp rx(QLatin1String("\\.([^\\s])")); QStringList values = FieldFormat::splitValue(entry->field(QStringLiteral("author"))); for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { (*it).replace(rx, QStringLiteral(". \\1")); } entry->setField(QStringLiteral("author"), values.join(FieldFormat::delimiterString())); } // UK puts the year in the title for some reason if(m_site == UK && coll->type() == Data::Collection::Video) { QRegExp rx(QLatin1String("\\[(\\d{4})\\]")); QString t = entry->title(); if(rx.indexIn(t) > -1) { QString y = rx.cap(1); t = t.remove(rx).simplified(); entry->setField(QStringLiteral("title"), t); if(entry->field(QStringLiteral("year")).isEmpty()) { entry->setField(QStringLiteral("year"), y); } } } // strip HTML from comments, or plot in movies // tentatively don't do this, looks like ECS 4 cleaned everything up /* if(coll->type() == Data::Collection::Video) { QString plot = entry->field(QLatin1String("plot")); plot.remove(stripHTML); entry->setField(QLatin1String("plot"), plot); } else if(coll->type() == Data::Collection::Game) { QString desc = entry->field(QLatin1String("description")); desc.remove(stripHTML); entry->setField(QLatin1String("description"), desc); } else { QString comments = entry->field(QLatin1String("comments")); comments.remove(stripHTML); entry->setField(QLatin1String("comments"), comments); } */ // myDebug() << entry->title(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); ++m_numResults; } // we might have gotten aborted if(!m_started) { return; } // are there any additional results to get? m_hasMoreResults = m_page * AMAZON_RETURNS_PER_REQUEST < m_total; const int currentTotal = qMin(m_total, m_limit); if(m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal) { int foundCount = (m_page-1) * AMAZON_RETURNS_PER_REQUEST + coll->entryCount(); message(i18n("Results from %1: %2/%3", source(), foundCount, m_total), MessageHandler::Status); ++m_page; m_countOffset = 0; doSearch(); } else if(request().value.count(QLatin1Char(';')) > 9) { // start new request after cutting off first 10 isbn values FetchRequest newRequest = request(); newRequest.value = request().value.section(QLatin1Char(';'), 10); startSearch(newRequest); } else { m_countOffset = m_entries.count() % AMAZON_RETURNS_PER_REQUEST; if(m_countOffset == 0) { ++m_page; // need to go to next page } stop(); } } Tellico::Data::EntryPtr AmazonFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; if(!entry) { myWarning() << "no entry in dict"; return entry; } // do what we can to remove useless keywords const int type = collectionType(); switch(type) { case Data::Collection::Book: case Data::Collection::ComicBook: case Data::Collection::Bibtex: if(optionalFields().contains(QStringLiteral("keyword"))) { StringSet newWords; const QStringList keywords = FieldFormat::splitValue(entry->field(QStringLiteral("keyword"))); foreach(const QString& keyword, keywords) { // the amazon2tellico stylesheet separates keywords with '/' const QStringList words = keyword.split(QLatin1Char('/')); foreach(const QString& word, words) { if(word == QLatin1String("General") || word == QLatin1String("Subjects") || word == QLatin1String("Par prix") || // french stuff word == QLatin1String("Divers") || // french stuff word.startsWith(QLatin1Char('(')) || word.startsWith(QLatin1String("Authors"))) { continue; } newWords.add(word); } } entry->setField(QStringLiteral("keyword"), newWords.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); break; case Data::Collection::Video: { const QString genres = QStringLiteral("genre"); QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); StringSet words; // only care about genres that have "Genres" in the amazon response // and take the first word after that for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { if((*it).indexOf(QLatin1String("Genres")) == -1) { continue; } // the amazon2tellico stylesheet separates words with '/' QStringList nodes = (*it).split(QLatin1Char('/')); for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { if(*it2 != QLatin1String("Genres")) { continue; } ++it2; if(it2 != nodes.end() && *it2 != QLatin1String("General")) { words.add(*it2); } break; // we're done } } entry->setField(genres, words.toList().join(FieldFormat::delimiterString())); // language tracks get duplicated, too words.clear(); words.add(FieldFormat::splitValue(entry->field(QStringLiteral("language")))); entry->setField(QStringLiteral("language"), words.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("plot"), Tellico::decodeHTML(entry->field(QStringLiteral("plot")))); break; case Data::Collection::Album: { const QString genres = QStringLiteral("genre"); QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); StringSet words; // only care about genres that have "Styles" in the amazon response // and take the first word after that for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { if((*it).indexOf(QLatin1String("Styles")) == -1) { continue; } // the amazon2tellico stylesheet separates words with '/' QStringList nodes = (*it).split(QLatin1Char('/')); bool isStyle = false; for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { if(!isStyle) { if(*it2 == QLatin1String("Styles")) { isStyle = true; } continue; } if(*it2 != QLatin1String("General")) { words.add(*it2); } } } entry->setField(genres, words.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); break; case Data::Collection::Game: entry->setField(QStringLiteral("description"), Tellico::decodeHTML(entry->field(QStringLiteral("description")))); break; } // clean up the title parseTitle(entry); // also sometimes table fields have rows but no values Data::FieldList fields = entry->collection()->fields(); QRegExp blank(QLatin1String("[\\s") + FieldFormat::columnDelimiterString() + FieldFormat::delimiterString() + QLatin1String("]+")); // only white space, column separators and value separators foreach(Data::FieldPtr fIt, fields) { if(fIt->type() != Data::Field::Table) { continue; } if(blank.exactMatch(entry->field(fIt))) { entry->setField(fIt, QString()); } } QString imageURL; switch(m_imageSize) { case SmallImage: imageURL = entry->field(QStringLiteral("small-image")); break; case MediumImage: imageURL = entry->field(QStringLiteral("medium-image")); break; case LargeImage: imageURL = entry->field(QStringLiteral("large-image")); break; case NoImage: default: break; } // myDebug() << "grabbing " << imageURL.toDisplayString(); if(!imageURL.isEmpty()) { QString id = ImageFactory::addImage(QUrl::fromUserInput(imageURL), true); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor // all relevant collection types have cover fields entry->setField(QStringLiteral("cover"), id); } } // don't want to show image urls in the fetch dialog entry->setField(QStringLiteral("small-image"), QString()); entry->setField(QStringLiteral("medium-image"), QString()); entry->setField(QStringLiteral("large-image"), QString()); return entry; } void AmazonFetcher::initXSLTHandler() { QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("amazon2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate amazon2tellico.xsl."; return; } QUrl u = QUrl::fromLocalFile(xsltfile); delete m_xsltHandler; m_xsltHandler = new XSLTHandler(u); if(!m_xsltHandler->isValid()) { myWarning() << "error in amazon2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } Tellico::Fetch::FetchRequest AmazonFetcher::updateRequest(Data::EntryPtr entry_) { const int type = entry_->collection()->type(); const QString t = entry_->field(QStringLiteral("title")); if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(Fetch::ISBN, isbn); } const QString a = entry_->field(QStringLiteral("author")); if(!a.isEmpty()) { return t.isEmpty() ? FetchRequest(Fetch::Person, a) : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); } } else if(type == Data::Collection::Album) { const QString a = entry_->field(QStringLiteral("artist")); if(!a.isEmpty()) { return t.isEmpty() ? FetchRequest(Fetch::Person, a) : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); } } // optimistically try searching for title and rely on Collection::sameEntry() to figure things out if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } void AmazonFetcher::parseTitle(Tellico::Data::EntryPtr entry_) { // assume that everything in brackets or parentheses is extra QRegExp rx(QLatin1String("[\\(\\[](.*)[\\)\\]]")); rx.setMinimal(true); QString title = entry_->field(QStringLiteral("title")); int pos = rx.indexIn(title); while(pos > -1) { if(parseTitleToken(entry_, rx.cap(1))) { title.remove(pos, rx.matchedLength()); --pos; // search again there } pos = rx.indexIn(title, pos+1); } entry_->setField(QStringLiteral("title"), title.trimmed()); } bool AmazonFetcher::parseTitleToken(Tellico::Data::EntryPtr entry_, const QString& token_) { // myDebug() << "title token:" << token_; // if res = true, then the token gets removed from the title bool res = false; if(token_.indexOf(QLatin1String("widescreen"), 0, Qt::CaseInsensitive) > -1 || token_.indexOf(i18n("Widescreen"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("widescreen"), QStringLiteral("true")); // res = true; leave it in the title } else if(token_.indexOf(QLatin1String("full screen"), 0, Qt::CaseInsensitive) > -1) { // skip, but go ahead and remove from title res = true; } else if(token_.indexOf(QLatin1String("import"), 0, Qt::CaseInsensitive) > -1) { // skip, but go ahead and remove from title res = true; } if(token_.indexOf(QLatin1String("blu-ray"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("Blu-ray")); res = true; } else if(token_.indexOf(QLatin1String("hd dvd"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("HD DVD")); res = true; } else if(token_.indexOf(QLatin1String("vhs"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("VHS")); res = true; } if(token_.indexOf(QLatin1String("director's cut"), 0, Qt::CaseInsensitive) > -1 || token_.indexOf(i18n("Director's Cut"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("directors-cut"), QStringLiteral("true")); // res = true; leave it in the title } if(token_.toLower() == QLatin1String("ntsc")) { entry_->setField(QStringLiteral("format"), i18n("NTSC")); res = true; } if(token_.toLower() == QLatin1String("dvd")) { entry_->setField(QStringLiteral("medium"), i18n("DVD")); res = true; } static QRegExp regionRx(QLatin1String("Region [1-9]")); if(regionRx.indexIn(token_) > -1) { entry_->setField(QStringLiteral("region"), i18n(regionRx.cap(0).toUtf8().constData())); res = true; } if(entry_->collection()->type() == Data::Collection::Game) { Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform")); if(f && f->allowed().contains(token_)) { res = true; } } return res; } QString AmazonFetcher::secretKey() const { return QString::fromUtf8(m_amazonKey); } //static QString AmazonFetcher::defaultName() { return i18n("Amazon.com Web Services"); } QString AmazonFetcher::defaultIcon() { return favIcon("http://www.amazon.com"); } Tellico::StringHash AmazonFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("keyword")] = i18n("Keywords"); return hash; } Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const { return new AmazonFetcher::ConfigWidget(parent_, this); } AmazonFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", AmazonFetcher::defaultName(), QLatin1String("https://affiliate-program.amazon.com/gp/flex/advertising/api/sign-in.html")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_accessEdit = new QLineEdit(optionsWidget()); - connect(m_accessEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_accessEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_accessEdit, row, 1); QString w = i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key."); label->setWhatsThis(w); m_accessEdit->setWhatsThis(w); label->setBuddy(m_accessEdit); label = new QLabel(i18n("Secret key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_secretKeyEdit = new QLineEdit(optionsWidget()); // m_secretKeyEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); - connect(m_secretKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_secretKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_secretKeyEdit, row, 1); label->setWhatsThis(w); m_secretKeyEdit->setWhatsThis(w); label->setBuddy(m_secretKeyEdit); label = new QLabel(i18n("Country: "), optionsWidget()); l->addWidget(label, ++row, 0); m_siteCombo = new GUI::ComboBox(optionsWidget()); for(int i = 0; i < XX; ++i) { const AmazonFetcher::SiteData& siteData = AmazonFetcher::siteData(i); QIcon icon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/locale/countries/%1/flag.png").arg(siteData.country))); m_siteCombo->addItem(icon, siteData.countryName, i); m_siteCombo->model()->sort(0); } - connect(m_siteCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); - connect(m_siteCombo, SIGNAL(activated(int)), SLOT(slotSiteChanged())); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSiteChanged); l->addWidget(m_siteCombo, row, 1); w = i18n("Amazon.com provides data from several different localized sites. Choose the one " "you wish to use for this data source."); label->setWhatsThis(w); m_siteCombo->setWhatsThis(w); label->setBuddy(m_siteCombo); label = new QLabel(i18n("&Image size: "), optionsWidget()); l->addWidget(label, ++row, 0); m_imageCombo = new GUI::ComboBox(optionsWidget()); m_imageCombo->addItem(i18n("Small Image"), SmallImage); m_imageCombo->addItem(i18n("Medium Image"), MediumImage); m_imageCombo->addItem(i18n("Large Image"), LargeImage); m_imageCombo->addItem(i18n("No Image"), NoImage); - connect(m_imageCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified); l->addWidget(m_imageCombo, row, 1); w = i18n("The cover image may be downloaded as well. However, too many large images in the " "collection may degrade performance."); label->setWhatsThis(w); m_imageCombo->setWhatsThis(w); label->setBuddy(m_imageCombo); label = new QLabel(i18n("&Associate's ID: "), optionsWidget()); l->addWidget(label, ++row, 0); m_assocEdit = new QLineEdit(optionsWidget()); - connect(m_assocEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + void (QLineEdit::* textChanged)(const QString&) = &QLineEdit::textChanged; + connect(m_assocEdit, textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_assocEdit, row, 1); w = i18n("The associate's id identifies the person accessing the Amazon.com Web Services, and is included " "in any links to the Amazon.com site."); label->setWhatsThis(w); m_assocEdit->setWhatsThis(w); label->setBuddy(m_assocEdit); l->setRowStretch(++row, 10); if(fetcher_) { m_siteCombo->setCurrentData(fetcher_->m_site); m_accessEdit->setText(fetcher_->m_access); m_secretKeyEdit->setText(fetcher_->secretKey()); m_assocEdit->setText(fetcher_->m_assoc); m_imageCombo->setCurrentData(fetcher_->m_imageSize); } else { // defaults m_assocEdit->setText(QLatin1String(AMAZON_ASSOC_TOKEN)); m_imageCombo->setCurrentData(MediumImage); } addFieldsWidget(AmazonFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); KAcceleratorManager::manage(optionsWidget()); } void AmazonFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { int n = m_siteCombo->currentData().toInt(); config_.writeEntry("Site", n); QString s = m_accessEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("AccessKey", s); } QByteArray b = m_secretKeyEdit->text().trimmed().toUtf8(); if(!b.isEmpty()) { config_.writeEntry("SecretKey", b); } s = m_assocEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("AssocToken", s); } n = m_imageCombo->currentData().toInt(); config_.writeEntry("Image Size", n); } QString AmazonFetcher::ConfigWidget::preferredName() const { return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title; } void AmazonFetcher::ConfigWidget::slotSiteChanged() { emit signalName(preferredName()); } diff --git a/src/fetch/animenfofetcher.cpp b/src/fetch/animenfofetcher.cpp index c6b292c8..c9956341 100644 --- a/src/fetch/animenfofetcher.cpp +++ b/src/fetch/animenfofetcher.cpp @@ -1,514 +1,514 @@ /*************************************************************************** Copyright (C) 2006-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 . * * * ***************************************************************************/ #include "animenfofetcher.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collections/bookcollection.h" #include "../collections/videocollection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../images/imagefactory.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* ANIMENFO_BASE_URL = "http://www.animenfo.com/search.php"; } using namespace Tellico; using Tellico::Fetch::AnimeNfoFetcher; AnimeNfoFetcher::AnimeNfoFetcher(QObject* parent_) : Fetcher(parent_), m_started(false) { } AnimeNfoFetcher::~AnimeNfoFetcher() { } QString AnimeNfoFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool AnimeNfoFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex || type == Data::Collection::Video; } void AnimeNfoFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void AnimeNfoFetcher::search() { m_started = true; m_matches.clear(); QUrl u(QString::fromLatin1(ANIMENFO_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("action"), QStringLiteral("Go")); q.addQueryItem(QStringLiteral("option"), QStringLiteral("keywords")); switch(request().collectionType) { case Data::Collection::Book: q.addQueryItem(QStringLiteral("queryin"), QStringLiteral("manga_titles")); break; case Data::Collection::Video: q.addQueryItem(QStringLiteral("queryin"), QStringLiteral("anime_titles")); break; default: myWarning() << "collection type not valid:" << request().collectionType; stop(); return; } switch(request().key) { case Keyword: q.addQueryItem(QStringLiteral("query"), request().value); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } u.setQuery(q); // myDebug() << "url:" << u; m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &AnimeNfoFetcher::slotComplete); } void AnimeNfoFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void AnimeNfoFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } const QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; QString s = Tellico::decodeHTML(data); #if 0 myWarning() << "Remove debug from animenfofetcher.cpp"; QFile f(QLatin1String("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << s; } f.close(); #endif QRegExp infoRx(QLatin1String("]*class\\s*=\\s*[\"']anime_info[\"'][^>]*>(.*)"), Qt::CaseInsensitive); infoRx.setMinimal(true); QRegExp anchorRx(QLatin1String("]*href\\s*=\\s*[\"'](.*)[\"'][^>]*>(.*)"), Qt::CaseInsensitive); anchorRx.setMinimal(true); QRegExp yearRx(QLatin1String("\\d{4}")); // search page comes in groups of threes int n = 0; QString u, t, y; for(int pos = infoRx.indexIn(s); m_started && pos > -1; pos = infoRx.indexIn(s, pos+1)) { if(n == 0 && !u.isEmpty()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), t, y); QUrl url = QUrl(QString::fromLatin1(ANIMENFO_BASE_URL)).resolved(QUrl(u)); url.setQuery(QString()); m_matches.insert(r->uid, url); // don't emit signal until after putting url in matches hash emit signalResultFound(r); u.clear(); t.clear(); y.clear(); } switch(n) { case 0: // title and url { int pos2 = anchorRx.indexIn(infoRx.cap(1)); if(pos2 > -1) { u = anchorRx.cap(1); t = anchorRx.cap(2); } } break; case 1: // don't case break; case 2: if(yearRx.exactMatch(infoRx.cap(1))) { y = infoRx.cap(1); } break; } n = (n+1)%3; } // grab last response if(!u.isEmpty()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), t, y, QString()); QUrl url = QUrl(QString::fromLatin1(ANIMENFO_BASE_URL)).resolved(QUrl(u)); url.setQuery(QString()); m_matches.insert(r->uid, url); // don't emit signal until after putting url in matches hash emit signalResultFound(r); } stop(); } Tellico::Data::EntryPtr AnimeNfoFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } QUrl url = m_matches[uid_]; if(url.isEmpty()) { myWarning() << "no url in map"; return Data::EntryPtr(); } QString results = Tellico::decodeHTML(FileHandler::readTextFile(url, true, true)); if(results.isEmpty()) { myDebug() << "no text results"; return Data::EntryPtr(); } #if 0 myWarning() << "Remove debug from animenfofetcher.cpp"; QFile f(QLatin1String("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << results; } f.close(); #endif entry = parseEntry(results, url); if(!entry) { myDebug() << "error in processing entry"; return Data::EntryPtr(); } m_entries.insert(uid_, entry); // keep for later return entry; } Tellico::Data::EntryPtr AnimeNfoFetcher::parseEntry(const QString& str_, const QUrl& url_) { // myDebug(); // class might be anime_info_top QRegExp infoRx(QLatin1String("]*class\\s*=\\s*[\"']anime_info[^>]*>(.*)"), Qt::CaseInsensitive); infoRx.setMinimal(true); QRegExp tagRx(QLatin1String("<.*>")); tagRx.setMinimal(true); QRegExp anchorRx(QLatin1String("]*href\\s*=\\s*[\"'](.*)[\"'][^>]*>(.*)"), Qt::CaseInsensitive); anchorRx.setMinimal(true); QRegExp jsRx(QLatin1String(""), Qt::CaseInsensitive); jsRx.setMinimal(true); QString s = str_; s.remove(jsRx); Data::CollPtr coll; switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: coll = Data::CollPtr(new Data::BookCollection(true)); break; case Data::Collection::Video: coll = Data::CollPtr(new Data::VideoCollection(true)); break; default: return Data::EntryPtr(); } // add new fields Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); coll->addField(f); f = new Data::Field(QStringLiteral("alttitle"), i18n("Alternative Titles"), Data::Field::Table); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); f = new Data::Field(QStringLiteral("distributor"), i18n("Distributor")); f->setCategory(i18n("Other People")); f->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); f->setFormatType(FieldFormat::FormatPlain); coll->addField(f); f = new Data::Field(QStringLiteral("episodes"), i18n("Episodes"), Data::Field::Number); f->setCategory(i18n("Features")); coll->addField(f); f = new Data::Field(QStringLiteral("animenfo"), i18n("AnimeNfo Link"), Data::Field::URL); f->setCategory(i18n("General")); coll->addField(f); f = new Data::Field(QStringLiteral("animenfo-rating"), i18n("AnimeNfo Rating"), Data::Field::Rating); f->setCategory(i18n("General")); f->setProperty(QStringLiteral("maximum"), QStringLiteral("10")); coll->addField(f); // map captions in HTML to field names QHash fieldMap; fieldMap.insert(QStringLiteral("Title"), QStringLiteral("title")); fieldMap.insert(QStringLiteral("Japanese Title"), QStringLiteral("origtitle")); fieldMap.insert(QStringLiteral("Total Episodes"), QStringLiteral("episodes")); fieldMap.insert(QStringLiteral("Category"), QStringLiteral("keyword")); fieldMap.insert(QStringLiteral("Genres"), QStringLiteral("genre")); fieldMap.insert(QStringLiteral("Genre"), QStringLiteral("genre")); fieldMap.insert(QStringLiteral("Studio"), QStringLiteral("studio")); fieldMap.insert(QStringLiteral("US Distribution"), QStringLiteral("distributor")); fieldMap.insert(QStringLiteral("Author"), QStringLiteral("author")); fieldMap.insert(QStringLiteral("Publisher"), QStringLiteral("publisher")); fieldMap.insert(QStringLiteral("Director"), QStringLiteral("director")); fieldMap.insert(QStringLiteral("Script"), QStringLiteral("writer")); fieldMap.insert(QStringLiteral("Music"), QStringLiteral("composer")); fieldMap.insert(QStringLiteral("User Rating"), QStringLiteral("animenfo-rating")); switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: fieldMap.insert(QStringLiteral("Year Published"), QStringLiteral("pub_year")); break; case Data::Collection::Video: fieldMap.insert(QStringLiteral("Year Published"), QStringLiteral("year")); break; default: break; } Data::EntryPtr entry(new Data::Entry(coll)); QString fullTitle; int n = 0; QString key, value; for(int pos = infoRx.indexIn(s); pos > -1; pos = infoRx.indexIn(s, pos+1)) { if(n == 0 && !key.isEmpty()) { if(fieldMap.contains(key)) { value = value.simplified(); if(value.endsWith(QLatin1Char(';'))) { value.chop(1); } if(!value.isEmpty() && value != QLatin1String("-")) { const QString fieldName = fieldMap.value(key); if(key == QLatin1String("Title")) { // strip possible trailing year, etc. fullTitle = value; value.remove(QRegExp(QLatin1String("\\s*\\([^)]*\\)$"))); entry->setField(fieldName, value); } else if(key == QLatin1String("Total Episodes")) { // strip possible trailing text value.remove(QRegExp(QLatin1String("[\\D].*$"))); entry->setField(fieldName, value); } else if(key == QLatin1String("User Rating")) { QRegExp rating(QLatin1String("^(.*)/10")); if(rating.indexIn(value) > -1) { const double d = rating.cap(1).toDouble(); entry->setField(fieldName, QString::number(static_cast(d+0.5))); } } else if(key == QLatin1String("Year Published")) { // strip possible trailing text value.remove(QRegExp(QLatin1String("[\\D;].*$"))); entry->setField(fieldName, value); } else { entry->setField(fieldName, value); } if(fieldName == QLatin1String("studio") || fieldName == QLatin1String("genre") || fieldName == QLatin1String("script") || fieldName == QLatin1String("distributor") || fieldName == QLatin1String("director") || fieldName == QLatin1String("writer") || fieldName == QLatin1String("author") || fieldName == QLatin1String("publisher") || fieldName == QLatin1String("composer")) { QStringList values = entry->field(fieldName).split(QRegExp(QLatin1String("\\s*,\\s*"))); entry->setField(fieldName, values.join(FieldFormat::delimiterString())); } } } key.clear(); value.clear(); } switch(n) { case 0: key = infoRx.cap(1).remove(tagRx); break; case 1: value = infoRx.cap(1).replace(QLatin1String("
"), QLatin1String("; ")).remove(tagRx); break; } n = (n+1)%2; } entry->setField(QStringLiteral("animenfo"), url_.url()); // image QRegExp imgRx(QStringLiteral("]*src\\s*=\\s*[\"']([^>]*)[\"']\\s+[^>]*alt\\s*=\\s*[\"']%1[\"']") .arg(QRegExp::escape(fullTitle)), Qt::CaseInsensitive); imgRx.setMinimal(true); int pos = imgRx.indexIn(s); if(pos > -1) { QUrl imgURL = QUrl(QLatin1String(ANIMENFO_BASE_URL)).resolved(QUrl(imgRx.cap(1))); QString id = ImageFactory::addImage(imgURL, true); if(!id.isEmpty()) { entry->setField(QStringLiteral("cover"), id); } else { myDebug() << "bad cover" << imgURL.url(); } } // now look for alternative titles and plot const QString a = QStringLiteral("Alternative titles"); pos = s.indexOf(a, 0, Qt::CaseInsensitive); if(pos > -1) { pos += a.length(); int pos2 = s.indexOf(QLatin1String(" -1) { value = s.mid(pos, pos2-pos).simplified(); value.replace(QLatin1String("
"), FieldFormat::rowDelimiterString()); value = value.remove(tagRx).trimmed(); entry->setField(QStringLiteral("alttitle"), value); } } pos = s.indexOf(QLatin1String("Description"), pos > -1 ? pos : 0); if(pos > -1) { QRegExp descRx(QLatin1String("]*class\\s*=\\s*[\"']description[\"'].*>(.*) -1) { entry->setField(QStringLiteral("plot"), descRx.cap(1).remove(tagRx).simplified()); } } pos = s.indexOf(QLatin1String("Voice Talent")); if(pos > -1) { QRegExp charRx(QLatin1String("(.*)"), Qt::CaseInsensitive); charRx.setMinimal(true); QRegExp voiceRx(QLatin1String("(.*)"), Qt::CaseInsensitive); voiceRx.setMinimal(true); QStringList castLines; for(pos = s.indexOf(charRx, pos); pos > -1; pos = s.indexOf(charRx, pos+1)) { if(voiceRx.indexIn(s, pos) > -1) { castLines << voiceRx.cap(1) + FieldFormat::columnDelimiterString() + charRx.cap(1); } } entry->setField(QStringLiteral("cast"), castLines.join(FieldFormat::rowDelimiterString())); } return entry; } Tellico::Fetch::FetchRequest AnimeNfoFetcher::updateRequest(Data::EntryPtr entry_) { QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Keyword, t); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* AnimeNfoFetcher::configWidget(QWidget* parent_) const { return new AnimeNfoFetcher::ConfigWidget(parent_, this); } QString AnimeNfoFetcher::defaultName() { return QStringLiteral("AnimeNfo.com"); } QString AnimeNfoFetcher::defaultIcon() { return favIcon("http://animenfo.com"); } //static Tellico::StringHash AnimeNfoFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("distributor")] = i18n("Distributor"); hash[QStringLiteral("episodes")] = i18n("Episodes"); hash[QStringLiteral("origtitle")] = i18n("Original Title"); hash[QStringLiteral("alttitle")] = i18n("Alternative Titles"); hash[QStringLiteral("animenfo-rating")] = i18n("AnimeNfo Rating"); hash[QStringLiteral("animenfo")] = i18n("AnimeNfo Link"); return hash; } AnimeNfoFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AnimeNfoFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(AnimeNfoFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString AnimeNfoFetcher::ConfigWidget::preferredName() const { return AnimeNfoFetcher::defaultName(); } diff --git a/src/fetch/arxivfetcher.cpp b/src/fetch/arxivfetcher.cpp index 92462d8b..85e6754e 100644 --- a/src/fetch/arxivfetcher.cpp +++ b/src/fetch/arxivfetcher.cpp @@ -1,337 +1,337 @@ /*************************************************************************** Copyright (C) 2007-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 . * * * ***************************************************************************/ #include "arxivfetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../utils/datafileregistry.h" #include "../collection.h" #include "../entry.h" #include "../core/netaccess.h" #include "../images/imagefactory.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int ARXIV_RETURNS_PER_REQUEST = 20; static const char* ARXIV_BASE_URL = "http://export.arxiv.org/api/query"; } using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::ArxivFetcher; ArxivFetcher::ArxivFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_start(0), m_total(-1), m_job(nullptr), m_started(false) { } ArxivFetcher::~ArxivFetcher() { delete m_xsltHandler; m_xsltHandler = nullptr; } QString ArxivFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool ArxivFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void ArxivFetcher::readConfigHook(const KConfigGroup&) { } void ArxivFetcher::search() { m_started = true; m_start = 0; m_total = -1; doSearch(); } void ArxivFetcher::continueSearch() { m_started = true; doSearch(); } void ArxivFetcher::doSearch() { QUrl u = searchURL(request().key, request().value); if(u.isEmpty()) { stop(); return; } m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &ArxivFetcher::slotComplete); } void ArxivFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void ArxivFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from arxivfetcher.cpp"; QFile f(QLatin1String("/tmp/test.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return; } } if(m_total == -1) { QDomDocument dom; if(!dom.setContent(data, true /*namespace*/)) { myWarning() << "server did not return valid XML."; stop(); return; } // total is top level element, with attribute totalResultsAvailable QDomNodeList list = dom.elementsByTagNameNS(QStringLiteral("http://a9.com/-/spec/opensearch/1.1/"), QStringLiteral("totalResults")); if(list.count() > 0) { m_total = list.item(0).toElement().text().toInt(); } } // assume result is always utf-8 QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size())); Import::TellicoImporter imp(str); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no valid result"; stop(); return; } foreach(Data::EntryPtr entry, coll->entries()) { if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } m_start = m_entries.count(); m_hasMoreResults = m_start < m_total; stop(); // required } Tellico::Data::EntryPtr ArxivFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; // if URL but no cover image, fetch it if(!entry->field(QStringLiteral("url")).isEmpty()) { Data::CollPtr coll = entry->collection(); Data::FieldPtr field = coll->fieldByName(QStringLiteral("cover")); if(!field && !coll->imageFields().isEmpty()) { field = coll->imageFields().front(); } else if(!field) { field = new Data::Field(QStringLiteral("cover"), i18n("Front Cover"), Data::Field::Image); coll->addField(field); } if(entry->field(field).isEmpty()) { QPixmap pix = NetAccess::filePreview(QUrl::fromUserInput(entry->field(QStringLiteral("url")))); if(!pix.isNull()) { QString id = ImageFactory::addImage(pix, QStringLiteral("PNG")); if(!id.isEmpty()) { entry->setField(field, id); } } } } QRegExp versionRx(QLatin1String("v\\d+$")); // if the original search was not for a versioned ID, remove it if(request().key != ArxivID || !request().value.contains(versionRx)) { QString arxiv = entry->field(QStringLiteral("arxiv")); arxiv.remove(versionRx); entry->setField(QStringLiteral("arxiv"), arxiv); } return entry; } void ArxivFetcher::initXSLTHandler() { QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("arxiv2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate arxiv2tellico.xsl."; return; } QUrl u = QUrl::fromLocalFile(xsltfile); delete m_xsltHandler; m_xsltHandler = new XSLTHandler(u); if(!m_xsltHandler->isValid()) { myWarning() << "error in arxiv2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } QUrl ArxivFetcher::searchURL(FetchKey key_, const QString& value_) const { QUrl u(QString::fromLatin1(ARXIV_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("start"), QString::number(m_start)); q.addQueryItem(QStringLiteral("max_results"), QString::number(ARXIV_RETURNS_PER_REQUEST)); // quotes should be used if spaces are present QString value = value_; value.replace(QLatin1Char(' '), QLatin1Char('+')); // seems to have problems with dashes, too value.replace(QLatin1Char('-'), QLatin1Char('+')); QString query; switch(key_) { case Title: query = QStringLiteral("ti:%1").arg(value); break; case Person: query = QStringLiteral("au:%1").arg(value); break; case Keyword: // keyword gets to use all the words without being quoted query = QStringLiteral("all:%1").arg(value); break; case ArxivID: { // remove prefix and/or version number QString value = value_; value.remove(QRegExp(QLatin1String("^arxiv:"), Qt::CaseInsensitive)); value.remove(QRegExp(QLatin1String("v\\d+$"))); query = QStringLiteral("id:%1").arg(value); } break; default: myWarning() << "key not recognized: " << request().key; return QUrl(); } q.addQueryItem(QStringLiteral("search_query"), query); u.setQuery(q); // myDebug() << "url: " << u; return u; } Tellico::Fetch::FetchRequest ArxivFetcher::updateRequest(Data::EntryPtr entry_) { QString id = entry_->field(QStringLiteral("arxiv")); if(!id.isEmpty()) { // remove prefix and/or version number id.remove(QRegExp(QLatin1String("^arxiv:"), Qt::CaseInsensitive)); id.remove(QRegExp(QLatin1String("v\\d+$"))); return FetchRequest(Fetch::ArxivID, id); } // optimistically try searching for title and rely on Collection::sameEntry() to figure things out QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* ArxivFetcher::configWidget(QWidget* parent_) const { return new ArxivFetcher::ConfigWidget(parent_, this); } QString ArxivFetcher::defaultName() { return QStringLiteral("arXiv.org"); // no translation } QString ArxivFetcher::defaultIcon() { return favIcon("http://arxiv.org"); } ArxivFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ArxivFetcher*) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); } void ArxivFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString ArxivFetcher::ConfigWidget::preferredName() const { return ArxivFetcher::defaultName(); } diff --git a/src/fetch/bedethequefetcher.cpp b/src/fetch/bedethequefetcher.cpp index 1174b2e4..f150dea2 100644 --- a/src/fetch/bedethequefetcher.cpp +++ b/src/fetch/bedethequefetcher.cpp @@ -1,474 +1,474 @@ /*************************************************************************** Copyright (C) 2016 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 "bedethequefetcher.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../utils/isbnvalidator.h" #include "../collections/comicbookcollection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../images/imagefactory.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include namespace { static const char* BD_BASE_URL = "https://m.bedetheque.com/album"; } using namespace Tellico; using Tellico::Fetch::BedethequeFetcher; BedethequeFetcher::BedethequeFetcher(QObject* parent_) : Fetcher(parent_), m_total(0), m_started(false) { } BedethequeFetcher::~BedethequeFetcher() { } QString BedethequeFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } Fetch::Type BedethequeFetcher::type() const { return Bedetheque; } bool BedethequeFetcher::canFetch(int type) const { return type == Data::Collection::ComicBook; } // No UPC or Raw for now. bool BedethequeFetcher::canSearch(FetchKey k) const { return k == Title || k == Keyword || k == ISBN; } void BedethequeFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void BedethequeFetcher::search() { m_started = true; m_matches.clear(); // special case for updates which include the BD link as Raw request if(request().key == Raw) { QUrl u(request().value); u.setHost(QStringLiteral("m.bedetheque.com")); // use mobile site for easier parsing m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); m_job->addMetaData(QStringLiteral("referrer"), QString::fromLatin1(BD_BASE_URL)); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); // different slot here - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotLinkComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &BedethequeFetcher::slotLinkComplete); return; } QUrl u(QString::fromLatin1(BD_BASE_URL)); /* fetchToken(); if(m_token.isEmpty()) { myDebug() << "empty token"; stop(); return; } */ QUrlQuery q; switch(request().key) { case Title: q.addQueryItem(QStringLiteral("RechTitre"), request().value); break; case Keyword: q.addQueryItem(QStringLiteral("RechSerie"), request().value); break; case ISBN: q.addQueryItem(QStringLiteral("RechISBN"), ISBNValidator::cleanValue(request().value)); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } // q.addQueryItem(QLatin1String("csrf_token_bedetheque"), m_token); u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); m_job->addMetaData(QStringLiteral("referrer"), QString::fromLatin1(BD_BASE_URL)); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &BedethequeFetcher::slotComplete); } void BedethequeFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void BedethequeFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; QString output = Tellico::decodeHTML(data); #if 0 myWarning() << "Remove debug from bedethequefetcher.cpp"; QFile f(QString::fromLatin1("/tmp/testbd.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t << output; } f.close(); #endif const int pos_list = output.indexOf(QLatin1String("
  • "), 0, Qt::CaseInsensitive); if(pos_list == -1) { myDebug() << "No results found"; stop(); return; } const int pos_end = output.indexOf(QLatin1String(""), pos_list+1, Qt::CaseInsensitive); output = output.mid(pos_list, pos_end-pos_list); QString pat = QStringLiteral("https://m.bedetheque.com/BD"); QRegExp anchorRx(QLatin1String("]*href\\s*=\\s*[\"'](") + QRegExp::escape(pat) + QLatin1String("[^\"']*)\"[^>]*>(.*)(.*)<")); spanRx.setMinimal(true); for(int pos = anchorRx.indexIn(output); m_started && pos > -1; pos = anchorRx.indexIn(output, pos+anchorRx.matchedLength())) { QString url = anchorRx.cap(1); if(url.isEmpty()) { continue; } const QString result = anchorRx.cap(2); if(result.isEmpty()) { continue; } QString title; QStringList desc; for(int pos2 = spanRx.indexIn(result); pos2 > -1; pos2 = spanRx.indexIn(result, pos2+spanRx.matchedLength())) { QString cname = spanRx.cap(1); QString value = spanRx.cap(2); if(cname == QLatin1String("serie")) { desc += value; } else if(cname == QLatin1String("titre")) { title = value; } else if(cname == QLatin1String("dl")) { desc += value; } } if(!title.isEmpty() && !url.isEmpty()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), title, desc.join(QLatin1String(" "))); m_matches.insert(r->uid, QUrl(url)); emit signalResultFound(r); } } stop(); } // slot called after downloading the exact link void BedethequeFetcher::slotLinkComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; QString output = Tellico::decodeHTML(data); Data::EntryPtr entry = parseEntry(output); if(!entry) { myDebug() << "error in processing entry"; stop(); return; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_matches.insert(r->uid, QUrl(request().value)); m_entries.insert(r->uid, entry); // keep for later emit signalResultFound(r); stop(); } Tellico::Data::EntryPtr BedethequeFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } QUrl url = m_matches[uid_]; if(url.isEmpty()) { myWarning() << "no url in map"; return Data::EntryPtr(); } QString results = Tellico::decodeHTML(FileHandler::readDataFile(url, true)); if(results.isEmpty()) { myDebug() << "no text results"; return Data::EntryPtr(); } // myDebug() << url.url(); #if 0 myWarning() << "Remove debug from bedethequefetcher.cpp"; QFile f(QLatin1String("/tmp/testbditem.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << results; } f.close(); #endif entry = parseEntry(results); if(!entry) { myDebug() << "error in processing entry"; return Data::EntryPtr(); } m_entries.insert(uid_, entry); // keep for later return entry; } Tellico::Data::EntryPtr BedethequeFetcher::parseEntry(const QString& str_) { Data::CollPtr coll(new Data::ComicBookCollection(true)); // map captions in HTML to field names QHash fieldMap; fieldMap.insert(QStringLiteral("Série"), QStringLiteral("series")); fieldMap.insert(QStringLiteral("Titre"), QStringLiteral("title")); fieldMap.insert(QStringLiteral("Origine"), QStringLiteral("country")); // fieldMap.insert(QLatin1String("Format"), QLatin1String("binding")); fieldMap.insert(QStringLiteral("Scénario"), QStringLiteral("writer")); fieldMap.insert(QStringLiteral("Dessin"), QStringLiteral("artist")); fieldMap.insert(QStringLiteral("Dépot légal"), QStringLiteral("pub_year")); fieldMap.insert(QStringLiteral("Editeur"), QStringLiteral("publisher")); fieldMap.insert(QStringLiteral("Planches"), QStringLiteral("pages")); fieldMap.insert(QStringLiteral("Style"), QStringLiteral("genre")); fieldMap.insert(QStringLiteral("Tome"), QStringLiteral("issue")); fieldMap.insert(QStringLiteral("Collection"), QStringLiteral("edition")); if(optionalFields().contains(QStringLiteral("isbn"))) { Data::FieldPtr field = Data::Field::createDefaultField(Data::Field::IsbnField); coll->addField(field); fieldMap.insert(QStringLiteral("ISBN"), field->name()); } if(optionalFields().contains(QStringLiteral("colorist"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("colorist"), i18n("Colorist"))); field->setCategory(i18n("General")); field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); field->setFormatType(FieldFormat::FormatName); coll->addField(field); fieldMap.insert(QStringLiteral("Couleurs"), QStringLiteral("colorist")); } if(optionalFields().contains(QStringLiteral("lien-bel"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("lien-bel"), i18n("Bedetheque Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } QRegExp tagRx(QLatin1String("<.*>")); tagRx.setMinimal(true); QRegExp yearRx(QLatin1String("\\d{4}")); // the negative lookahead with "no-border" is for multiple values QString pat = QStringLiteral("(.+)
  • (?!\\s*
  • ::Iterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { QRegExp infoRx(pat.arg(it.key())); infoRx.setMinimal(true); if(infoRx.indexIn(str_) == -1) { continue; } if(it.value() == QLatin1String("pub_year")) { QString data = infoRx.cap(1).remove(tagRx).simplified(); if(yearRx.indexIn(data) > -1) { entry->setField(it.value(), yearRx.cap(0)); } } else if(it.value() == QLatin1String("writer") || it.value() == QLatin1String("artist") || it.value() == QLatin1String("publisher") || it.value() == QLatin1String("colorist")) { // catch multiple people QString value = infoRx.cap(1); // split the values with the "no-border" CSS value.replace(QLatin1String("
  • "), FieldFormat::delimiterString()); value = FieldFormat::fixupValue(value.remove(tagRx).simplified()); entry->setField(it.value(), value); } else if(it.value() == QLatin1String("genre")) { // replace comma with semi-colons to effectively split string values QString value = infoRx.cap(1).remove(tagRx).simplified(); value.replace(QLatin1String(", "), FieldFormat::delimiterString()); entry->setField(it.value(), value); } else { entry->setField(it.value(), infoRx.cap(1).remove(tagRx).simplified()); } // myDebug() << it.value() << entry->field(it.value()); } QRegExp imgRx(QLatin1String(" -1) { QUrl u(imgRx.cap(1)); QString id = ImageFactory::addImage(u, true); if(!id.isEmpty()) { entry->setField(QStringLiteral("cover"), id); } } if(optionalFields().contains(QStringLiteral("comments"))) { QRegExp chronRx(QLatin1String("La chronique\\s*
  • \\s*]*>(.*)")); chronRx.setMinimal(true); if(chronRx.indexIn(str_) > -1) { entry->setField(QStringLiteral("comments"), chronRx.cap(1).trimmed()); } } if(optionalFields().contains(QStringLiteral("lien-bel"))) { QRegExp linkRx(QLatin1String(" -1) { entry->setField(QStringLiteral("lien-bel"), linkRx.cap(1)); } } return entry; } Tellico::Fetch::FetchRequest BedethequeFetcher::updateRequest(Data::EntryPtr entry_) { QString l = entry_->field(QStringLiteral("lien-bel")); if(!l.isEmpty()) { return FetchRequest(Fetch::Raw, l); } QString i = entry_->field(QStringLiteral("isbn")); if(!i.isEmpty()) { return FetchRequest(Fetch::ISBN, i); } QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } void BedethequeFetcher::fetchToken() { QRegExp tokenRx(QLatin1String("name\\s*=\\s*\"csrf_token_bedetheque\"\\s*value\\s*=\\s*\"([^\"]+)\"")); const QUrl url(QStringLiteral("https://www.bedetheque.com/search/albums")); const QString text = FileHandler::readTextFile(url, true /*quiet*/); if(tokenRx.indexIn(text) > -1) { m_token = tokenRx.cap(1); } } Tellico::Fetch::ConfigWidget* BedethequeFetcher::configWidget(QWidget* parent_) const { return new BedethequeFetcher::ConfigWidget(parent_, this); } QString BedethequeFetcher::defaultName() { return QStringLiteral("Bedetheque"); } QString BedethequeFetcher::defaultIcon() { return favIcon("http://www.bedetheque.com"); } //static Tellico::StringHash BedethequeFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("colorist")] = i18n("Colorist"); hash[QStringLiteral("comments")] = i18n("Comments"); hash[QStringLiteral("isbn")] = i18n("ISBN#"); // use the field name that the bedetheque.py script did, to maintain backwards compatibility hash[QStringLiteral("lien-bel")] = i18n("Bedetheque Link"); return hash; } BedethequeFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BedethequeFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(BedethequeFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString BedethequeFetcher::ConfigWidget::preferredName() const { return BedethequeFetcher::defaultName(); } diff --git a/src/fetch/bibliosharefetcher.cpp b/src/fetch/bibliosharefetcher.cpp index 1033c244..e6e69390 100644 --- a/src/fetch/bibliosharefetcher.cpp +++ b/src/fetch/bibliosharefetcher.cpp @@ -1,215 +1,215 @@ /*************************************************************************** Copyright (C) 2011 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 "bibliosharefetcher.h" #include "../utils/isbnvalidator.h" #include "../entry.h" #include "../images/imagefactory.h" #include "../images/imageinfo.h" #include "../tellico_debug.h" #include #include #include #include #include #include namespace { static const char* BIBLIOSHARE_BASE_URL = "http://www.biblioshare.org/BNCServices/BNCServices.asmx/"; static const char* BIBLIOSHARE_TOKEN = "nsnqwebh87kstlty"; } using namespace Tellico; using Tellico::Fetch::BiblioShareFetcher; BiblioShareFetcher::BiblioShareFetcher(QObject* parent_) : XMLFetcher(parent_) , m_token(QLatin1String(BIBLIOSHARE_TOKEN)) { setLimit(1); setXSLTFilename(QStringLiteral("biblioshare2tellico.xsl")); } BiblioShareFetcher::~BiblioShareFetcher() { } QString BiblioShareFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } // https://www.booknetcanada.ca/get-a-token QString BiblioShareFetcher::attribution() const { return i18n("Data provided by BNC BiblioShare."); } bool BiblioShareFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } void BiblioShareFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("Token", BIBLIOSHARE_TOKEN); if(!k.isEmpty()) { m_token = k; } } QUrl BiblioShareFetcher::searchUrl() { QUrl u(QString::fromLatin1(BIBLIOSHARE_BASE_URL)); u.setPath(u.path() + QStringLiteral("BiblioSimple")); QUrlQuery q; q.addQueryItem(QStringLiteral("Token"), m_token); switch(request().key) { case ISBN: { // only grab first value QString v = request().value.section(QLatin1Char(';'), 0); v = ISBNValidator::isbn13(v); v.remove(QLatin1Char('-')); q.addQueryItem(QStringLiteral("EAN"), v); } break; default: return QUrl(); } u.setQuery(q); // myDebug() << "url:" << u.url(); return u; } Tellico::Data::EntryPtr BiblioShareFetcher::fetchEntryHookData(Data::EntryPtr entry_) { Q_ASSERT(entry_); if(!entry_) { myWarning() << "no entry"; return entry_; } // if the entry cover is not set, go ahead and try to fetch it if(entry_->field(QStringLiteral("cover")).isEmpty()) { QString isbn = ISBNValidator::cleanValue(entry_->field(QStringLiteral("isbn"))); if(!isbn.isEmpty()) { isbn = ISBNValidator::isbn13(isbn); isbn.remove(QLatin1Char('-')); QUrl imageUrl(QString::fromLatin1(BIBLIOSHARE_BASE_URL)); imageUrl.setPath(imageUrl.path() + QStringLiteral("Images")); QUrlQuery q; q.addQueryItem(QStringLiteral("Token"), m_token); q.addQueryItem(QStringLiteral("EAN"), isbn); // the actual values for SAN Thumbnail don't seem to matter, they just can't be empty q.addQueryItem(QStringLiteral("SAN"), QStringLiteral("string")); q.addQueryItem(QStringLiteral("Thumbnail"), QStringLiteral("cover")); imageUrl.setQuery(q); const QString id = ImageFactory::addImage(imageUrl, true); if(!id.isEmpty()) { // placeholder images are 120x120 or 1x1 Data::ImageInfo info = ImageFactory::imageInfo(id); if((info.width() != 120 || info.height() != 120) && (info.width() != 1 || info.height() != 1)) { entry_->setField(QStringLiteral("cover"), id); } } } } return entry_; } Tellico::Fetch::FetchRequest BiblioShareFetcher::updateRequest(Data::EntryPtr entry_) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(Fetch::ISBN, isbn); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* BiblioShareFetcher::configWidget(QWidget* parent_) const { return new BiblioShareFetcher::ConfigWidget(parent_, this); } QString BiblioShareFetcher::defaultName() { return QStringLiteral("BiblioShare"); } QString BiblioShareFetcher::defaultIcon() { return favIcon("https://www.booknetcanada.ca/biblioshare"); } BiblioShareFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BiblioShareFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QStringLiteral("https://www.booknetcanada.ca/get-a-token")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_tokenEdit = new QLineEdit(optionsWidget()); - connect(m_tokenEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_tokenEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_tokenEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_tokenEdit->setWhatsThis(w); label->setBuddy(m_tokenEdit); l->setRowStretch(++row, 10); if(fetcher_) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own if(fetcher_->m_token != QLatin1String(BIBLIOSHARE_TOKEN)) { m_tokenEdit->setText(fetcher_->m_token); } } } void BiblioShareFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString token = m_tokenEdit->text().trimmed(); if(!token.isEmpty()) { config_.writeEntry("Token", token); } } QString BiblioShareFetcher::ConfigWidget::preferredName() const { return BiblioShareFetcher::defaultName(); } diff --git a/src/fetch/bibsonomyfetcher.cpp b/src/fetch/bibsonomyfetcher.cpp index 01390f77..139058e5 100644 --- a/src/fetch/bibsonomyfetcher.cpp +++ b/src/fetch/bibsonomyfetcher.cpp @@ -1,195 +1,195 @@ /*************************************************************************** Copyright (C) 2007-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 . * * * ***************************************************************************/ #include "bibsonomyfetcher.h" #include "../translators/bibteximporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collection.h" #include "../entry.h" #include "../core/netaccess.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include namespace { // always bibtex static const char* BIBSONOMY_BASE_URL = "http://bibsonomy.org/"; static const int BIBSONOMY_MAX_RESULTS = 20; } using namespace Tellico; using Tellico::Fetch::BibsonomyFetcher; BibsonomyFetcher::BibsonomyFetcher(QObject* parent_) : Fetcher(parent_), m_job(nullptr), m_started(false) { } BibsonomyFetcher::~BibsonomyFetcher() { } QString BibsonomyFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool BibsonomyFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void BibsonomyFetcher::readConfigHook(const KConfigGroup&) { } void BibsonomyFetcher::search() { m_started = true; // myDebug() << "value = " << value_; QUrl u(QString::fromLatin1(BIBSONOMY_BASE_URL)); u.setPath(QStringLiteral("/bib/")); switch(request().key) { case Person: u.setPath(u.path() + QStringLiteral("author/%1").arg(request().value)); break; case Keyword: u.setPath(u.path() + QStringLiteral("search/%1").arg(request().value)); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } QUrlQuery q; q.addQueryItem(QStringLiteral("items"), QString::number(BIBSONOMY_MAX_RESULTS)); u.setQuery(q); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &BibsonomyFetcher::slotComplete); } void BibsonomyFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void BibsonomyFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; Import::BibtexImporter imp(QString::fromUtf8(data.constData(), data.size())); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no valid result"; stop(); return; } Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, Data::EntryPtr(entry)); emit signalResultFound(r); } stop(); // required } Tellico::Data::EntryPtr BibsonomyFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest BibsonomyFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Fetch::Keyword, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* BibsonomyFetcher::configWidget(QWidget* parent_) const { return new BibsonomyFetcher::ConfigWidget(parent_, this); } QString BibsonomyFetcher::defaultName() { return QStringLiteral("Bibsonomy"); } QString BibsonomyFetcher::defaultIcon() { return favIcon("https://www.bibsonomy.org"); } BibsonomyFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BibsonomyFetcher*) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); } void BibsonomyFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString BibsonomyFetcher::ConfigWidget::preferredName() const { return BibsonomyFetcher::defaultName(); } diff --git a/src/fetch/comicvinefetcher.cpp b/src/fetch/comicvinefetcher.cpp index 2ba3b24e..6bd8df18 100644 --- a/src/fetch/comicvinefetcher.cpp +++ b/src/fetch/comicvinefetcher.cpp @@ -1,278 +1,278 @@ /*************************************************************************** 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 "comicvinefetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace { static const int COMICVINE_MAX_RETURNS_TOTAL = 20; static const char* COMICVINE_API_URL = "https://www.comicvine.com/api"; static const char* COMICVINE_API_KEY = "6e4b19eeb8ccec8e2f026169d19adf57850d378e"; } using namespace Tellico; using Tellico::Fetch::ComicVineFetcher; ComicVineFetcher::ComicVineFetcher(QObject* parent_) : XMLFetcher(parent_) , m_total(-1) , m_apiKey(QLatin1String(COMICVINE_API_KEY)) { setLimit(COMICVINE_MAX_RETURNS_TOTAL); setXSLTFilename(QStringLiteral("comicvine2tellico.xsl")); } ComicVineFetcher::~ComicVineFetcher() { } QString ComicVineFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString ComicVineFetcher::attribution() const { return i18n("This information was freely provided by Comic Vine."); } bool ComicVineFetcher::canFetch(int type) const { return type == Data::Collection::ComicBook; } void ComicVineFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key", COMICVINE_API_KEY); if(!k.isEmpty()) { m_apiKey = k; } } void ComicVineFetcher::resetSearch() { m_total = -1; } QUrl ComicVineFetcher::searchUrl() { QUrl u(QString::fromLatin1(COMICVINE_API_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); switch(request().key) { case Keyword: u.setPath(u.path() + QStringLiteral("/search")); q.addQueryItem(QStringLiteral("query"), request().value); q.addQueryItem(QStringLiteral("resources"), QStringLiteral("issue")); break; default: myWarning() << "key not recognized: " << request().key; return QUrl(); } u.setQuery(q); // myDebug() << "url: " << u.url(); return u; } void ComicVineFetcher::parseData(QByteArray& data_) { Q_UNUSED(data_); } Tellico::Data::EntryPtr ComicVineFetcher::fetchEntryHookData(Data::EntryPtr entry_) { Q_ASSERT(entry_); const QString url = entry_->field(QStringLiteral("comicvine-api")); if(url.isEmpty()) { myDebug() << "no comicvine api url found"; return entry_; } QUrl u(url); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); u.setQuery(q); // myDebug() << "url: " << u; // quiet QString output = FileHandler::readXMLFile(u, true); #if 0 myWarning() << "Remove output debug from comicvinefetcher.cpp"; QFile f(QStringLiteral("/tmp/test2.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << output; } f.close(); #endif Import::TellicoImporter imp(xsltHandler()->applyStylesheet(output)); // be quiet when loading images imp.setOptions(imp.options() ^ Import::ImportShowImageErrors); Data::CollPtr coll = imp.collection(); if(!coll || coll->entryCount() == 0) { myWarning() << "no collection pointer"; return entry_; } if(coll->entryCount() > 1) { myDebug() << "weird, more than one entry found"; } // grab the publisher from the volume link const QString volUrl = entry_->field(QStringLiteral("comicvine-volume-api")); if(!volUrl.isEmpty()) { QUrl vu(volUrl); // easier to use JSON here QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("json")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); vu.setQuery(q); // myDebug() << "volume url: " << vu; QByteArray data = FileHandler::readDataFile(vu, true /* quiet */); #if 0 myWarning() << "Remove JSON output debug from comicvinefetcher.cpp"; QFile f2(QStringLiteral("/tmp/test2.json")); if(f2.open(QIODevice::WriteOnly)) { QTextStream t(&f2); t << data; } f2.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap map = doc.object().toVariantMap().value(QLatin1String("results")).toMap(); const QString pub = mapValue(map, "publisher", "name"); if(!pub.isEmpty()) { Data::EntryPtr e = coll->entries().front(); if(e) { e->setField(QStringLiteral("publisher"), pub); } } } // don't want to include api link coll->removeField(QStringLiteral("comicvine-api")); coll->removeField(QStringLiteral("comicvine-volume-api")); return coll->entries().front(); } Tellico::Fetch::FetchRequest ComicVineFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Keyword, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* ComicVineFetcher::configWidget(QWidget* parent_) const { return new ComicVineFetcher::ConfigWidget(parent_, this); } QString ComicVineFetcher::defaultName() { return QStringLiteral("Comic Vine"); } QString ComicVineFetcher::defaultIcon() { return favIcon("https://comicvine.gamespot.com"); } Tellico::StringHash ComicVineFetcher::allOptionalFields() { StringHash hash; // hash[QStringLiteral("colorist")] = i18n("Colorist"); hash[QStringLiteral("comicvine")] = i18n("Comic Vine Link"); return hash; } ComicVineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ComicVineFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("http://api.comicvine.com")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_apiKeyEdit->setWhatsThis(w); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); // now add additional fields widget addFieldsWidget(ComicVineFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); if(fetcher_) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own if(fetcher_->m_apiKey != QLatin1String(COMICVINE_API_KEY)) { m_apiKeyEdit->setText(fetcher_->m_apiKey); } } } void ComicVineFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString ComicVineFetcher::ConfigWidget::preferredName() const { return ComicVineFetcher::defaultName(); } diff --git a/src/fetch/configwidget.cpp b/src/fetch/configwidget.cpp index cae95b3d..811f91ce 100644 --- a/src/fetch/configwidget.cpp +++ b/src/fetch/configwidget.cpp @@ -1,102 +1,102 @@ /*************************************************************************** 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 . * * * ***************************************************************************/ #include "configwidget.h" #include #include #include #include #include using Tellico::Fetch::ConfigWidget; ConfigWidget::ConfigWidget(QWidget* parent_) : QWidget(parent_), m_modified(false), m_accepted(false) { QHBoxLayout* boxLayout = new QHBoxLayout(this); boxLayout->setSpacing(10); boxLayout->setMargin(0); QGroupBox* gvbox = new QGroupBox(i18n("Source Options"), this); boxLayout->addWidget(gvbox, 10 /*stretch*/); QVBoxLayout* vbox = new QVBoxLayout(); m_optionsWidget = new QWidget(gvbox); vbox->addWidget(m_optionsWidget); vbox->addStretch(1); gvbox->setLayout(vbox); } bool ConfigWidget::shouldSave() const { return m_modified && m_accepted; } void ConfigWidget::setAccepted(bool accepted_) { m_accepted = accepted_; } -void ConfigWidget::slotSetModified(bool modified_) { - m_modified = modified_; +void ConfigWidget::slotSetModified() { + m_modified = true; } void ConfigWidget::addFieldsWidget(const Tellico::StringHash& customFields_, const QStringList& fieldsToAdd_) { if(customFields_.isEmpty()) { return; } QGroupBox* gbox = new QGroupBox(i18n("Available Fields"), this); static_cast(layout())->addWidget(gbox); QVBoxLayout* vbox = new QVBoxLayout(); for(StringHash::ConstIterator it = customFields_.begin(); it != customFields_.end(); ++it) { QCheckBox* cb = new QCheckBox(it.value(), gbox); m_fields.insert(it.key(), cb); if(fieldsToAdd_.contains(it.key())) { cb->setChecked(true); } - connect(cb, SIGNAL(clicked()), SLOT(slotSetModified())); + connect(cb, &QAbstractButton::clicked, this, &ConfigWidget::slotSetModified); vbox->addWidget(cb); } vbox->addStretch(1); gbox->setLayout(vbox); KAcceleratorManager::manage(this); } void ConfigWidget::saveConfig(KConfigGroup& config_) { QStringList fields; QHash::const_iterator it = m_fields.constBegin(); for( ; it != m_fields.constEnd(); ++it) { if(it.value()->isChecked()) { fields << it.key(); } } config_.writeEntry(QStringLiteral("Custom Fields"), fields); saveConfigHook(config_); - slotSetModified(false); + m_modified = false; } QWidget* ConfigWidget::optionsWidget() { return m_optionsWidget; } diff --git a/src/fetch/configwidget.h b/src/fetch/configwidget.h index dc4352e4..9c5fa215 100644 --- a/src/fetch/configwidget.h +++ b/src/fetch/configwidget.h @@ -1,90 +1,90 @@ /*************************************************************************** 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 TELLICO_FETCHCONFIGWIDGET_H #define TELLICO_FETCHCONFIGWIDGET_H #include "../datavectors.h" #include #include #include class KConfigGroup; class QStringList; namespace Tellico { namespace Fetch { /** * @author Robby Stephenson */ class ConfigWidget : public QWidget { Q_OBJECT public: ConfigWidget(QWidget* parent); virtual ~ConfigWidget() {} bool shouldSave() const; void setAccepted(bool accepted); virtual void readConfig(const KConfigGroup&) {} /** * Saves any configuration options. The config group must be * set before calling this function. * * @param config_ The KConfig pointer */ void saveConfig(KConfigGroup& config); /** * Called when a fetcher data source is removed. Useful for any cleanup work necessary. * The ExecExternalFetcher might need to remove the script, for example. * Because of the way the ConfigDialog is setup, easier to have that in the ConfigWidget * class than in the Fetcher class itself */ virtual void removed() {} virtual QString preferredName() const = 0; Q_SIGNALS: void signalName(const QString& name); public Q_SLOTS: - void slotSetModified(bool modified = true); + void slotSetModified(); protected: QWidget* optionsWidget(); void addFieldsWidget(const StringHash& customFields, const QStringList& fieldsToAdd); virtual void saveConfigHook(KConfigGroup&) {} private: bool m_modified; bool m_accepted; QWidget* m_optionsWidget; QHash m_fields; }; } } #endif diff --git a/src/fetch/crossreffetcher.cpp b/src/fetch/crossreffetcher.cpp index 8e5b7986..24fabb04 100644 --- a/src/fetch/crossreffetcher.cpp +++ b/src/fetch/crossreffetcher.cpp @@ -1,382 +1,380 @@ /*************************************************************************** Copyright (C) 2007-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 . * * * ***************************************************************************/ #include "crossreffetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collection.h" #include "../entry.h" #include "../core/netaccess.h" #include "../images/imagefactory.h" #include "../utils/wallet.h" #include "../utils/datafileregistry.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define CROSSREF_USE_UNIXREF namespace { static const char* CROSSREF_BASE_URL = "http://www.crossref.org/openurl/"; } using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::CrossRefFetcher; CrossRefFetcher::CrossRefFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_job(nullptr), m_started(false) { } CrossRefFetcher::~CrossRefFetcher() { delete m_xsltHandler; m_xsltHandler = nullptr; } QString CrossRefFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool CrossRefFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void CrossRefFetcher::readConfigHook(const KConfigGroup& config_) { m_user = config_.readEntry("User"); m_password = config_.readEntry("Password"); m_email = config_.readEntry("Email"); } void CrossRefFetcher::search() { m_started = true; readWallet(); if(m_email.isEmpty() && (m_user.isEmpty() || m_password.isEmpty())) { myDebug() << i18n("%1 requires a username and password.", source()); message(i18n("%1 requires a username and password.", source()), MessageHandler::Error); stop(); return; } // myDebug() << "value = " << value_; QUrl u = searchURL(request().key, request().value); if(u.isEmpty()) { stop(); return; } m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &CrossRefFetcher::slotComplete); } void CrossRefFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void CrossRefFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from crossreffetcher.cpp"; QFile f(QLatin1String("/tmp/test.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return; } } // assume result is always utf-8 QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size())); Import::TellicoImporter imp(str); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no valid result"; stop(); return; } Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, Data::EntryPtr(entry)); emit signalResultFound(r); } stop(); // required } Tellico::Data::EntryPtr CrossRefFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; // if URL but no cover image, fetch it if(!entry->field(QStringLiteral("url")).isEmpty()) { Data::CollPtr coll = entry->collection(); Data::FieldPtr field = coll->fieldByName(QStringLiteral("cover")); if(!field && !coll->imageFields().isEmpty()) { field = coll->imageFields().front(); } else if(!field) { field = new Data::Field(QStringLiteral("cover"), i18n("Front Cover"), Data::Field::Image); coll->addField(field); } if(entry->field(field).isEmpty()) { QPixmap pix = NetAccess::filePreview(QUrl::fromUserInput(entry->field(QStringLiteral("url")))); if(!pix.isNull()) { QString id = ImageFactory::addImage(pix, QStringLiteral("PNG")); if(!id.isEmpty()) { entry->setField(field, id); } } } } return entry; } void CrossRefFetcher::initXSLTHandler() { #ifdef CROSSREF_USE_UNIXREF QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("unixref2tellico.xsl")); #else QString xsltfile = DataFileRegistry::self()->locate(QLatin1String("crossref2tellico.xsl")); #endif if(xsltfile.isEmpty()) { #ifdef CROSSREF_USE_UNIXREF myWarning() << "can not locate xslt file: unixref2tellico.xsl"; #else myWarning() << "can not locate xslt file: crossref2tellico.xsl"; #endif return; } QUrl u = QUrl::fromLocalFile(xsltfile); delete m_xsltHandler; m_xsltHandler = new XSLTHandler(u); if(!m_xsltHandler->isValid()) { myWarning() << "error in crossref2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } QUrl CrossRefFetcher::searchURL(FetchKey key_, const QString& value_) const { QUrl u(QString::fromLatin1(CROSSREF_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("noredirect"), QStringLiteral("true")); q.addQueryItem(QStringLiteral("multihit"), QStringLiteral("true")); #ifdef CROSSREF_USE_UNIXREF q.addQueryItem(QStringLiteral("format"), QStringLiteral("unixref")); #endif if(m_email.isEmpty()) { q.addQueryItem(QStringLiteral("pid"), QStringLiteral("%1:%2").arg(m_user, m_password)); } else { q.addQueryItem(QStringLiteral("pid"), m_email); } switch(key_) { case DOI: q.addQueryItem(QStringLiteral("rft_id"), QStringLiteral("info:doi/%1").arg(value_)); break; default: myWarning() << "key not recognized: " << key_; return QUrl(); } u.setQuery(q); // myDebug() << "url: " << u.url(); return u; } Tellico::Fetch::FetchRequest CrossRefFetcher::updateRequest(Data::EntryPtr entry_) { QString doi = entry_->field(QStringLiteral("doi")); if(!doi.isEmpty()) { return FetchRequest(Fetch::DOI, doi); } #if 0 // optimistically try searching for title and rely on Collection::sameEntry() to figure things out QString t = entry_->field(QLatin1String("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } #endif return FetchRequest(); } void CrossRefFetcher::readWallet() const { if(m_user.isEmpty() || m_password.isEmpty()) { QMap map = Wallet::self()->readWalletMap(QStringLiteral("crossref.org")); if(!map.isEmpty()) { m_user = map.value(QStringLiteral("username")); m_password = map.value(QStringLiteral("password")); } } } Tellico::Fetch::ConfigWidget* CrossRefFetcher::configWidget(QWidget* parent_) const { return new CrossRefFetcher::ConfigWidget(parent_, this); } QString CrossRefFetcher::defaultName() { return QStringLiteral("CrossRef"); // no translation } QString CrossRefFetcher::defaultIcon() { return favIcon("http://crossref.org"); } CrossRefFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const CrossRefFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = 0; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("http://www.crossref.org/requestaccount/")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("&Username: "), optionsWidget()); l->addWidget(label, ++row, 0); m_userEdit = new QLineEdit(optionsWidget()); - connect(m_userEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_userEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_userEdit, row, 1); QString w = i18n("A username and password is required to access the CrossRef service."); label->setWhatsThis(w); m_userEdit->setWhatsThis(w); label->setBuddy(m_userEdit); label = new QLabel(i18n("&Password: "), optionsWidget()); l->addWidget(label, ++row, 0); m_passEdit = new QLineEdit(optionsWidget()); // m_passEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); - connect(m_passEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_passEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_passEdit, row, 1); label->setWhatsThis(w); m_passEdit->setWhatsThis(w); label->setBuddy(m_passEdit); label = new QLabel(i18n("For some accounts, only an email address is required."), optionsWidget()); l->addWidget(label, ++row, 0, 1, 2); label = new QLabel(i18n("Email: "), optionsWidget()); l->addWidget(label, ++row, 0); m_emailEdit = new QLineEdit(optionsWidget()); - connect(m_emailEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_emailEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_emailEdit, row, 1); label->setBuddy(m_emailEdit); if(fetcher_) { fetcher_->readWallet(); // make sure that the wallet values are read m_userEdit->setText(fetcher_->m_user); m_passEdit->setText(fetcher_->m_password); m_emailEdit->setText(fetcher_->m_email); } } void CrossRefFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString s = m_userEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("User", s); } s = m_passEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("Password", s); } s = m_emailEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("Email", s); } - - slotSetModified(false); } QString CrossRefFetcher::ConfigWidget::preferredName() const { return CrossRefFetcher::defaultName(); } diff --git a/src/fetch/discogsfetcher.cpp b/src/fetch/discogsfetcher.cpp index cc12c1b5..01f9dfc5 100644 --- a/src/fetch/discogsfetcher.cpp +++ b/src/fetch/discogsfetcher.cpp @@ -1,466 +1,466 @@ /*************************************************************************** Copyright (C) 2008-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 . * * * ***************************************************************************/ #include // for TELLICO_VERSION #include "discogsfetcher.h" #include "../collections/musiccollection.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int DISCOGS_MAX_RETURNS_TOTAL = 20; static const char* DISCOGS_API_URL = "https://api.discogs.com"; } using namespace Tellico; using Tellico::Fetch::DiscogsFetcher; DiscogsFetcher::DiscogsFetcher(QObject* parent_) : Fetcher(parent_) , m_started(false) { } DiscogsFetcher::~DiscogsFetcher() { } QString DiscogsFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool DiscogsFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword; } bool DiscogsFetcher::canFetch(int type) const { return type == Data::Collection::Album; } void DiscogsFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key"); if(!k.isEmpty()) { m_apiKey = k; } } void DiscogsFetcher::search() { m_started = true; if(m_apiKey.isEmpty()) { myDebug() << "empty API key"; message(i18n("An access key is required to use this data source.") + QLatin1Char(' ') + i18n("Those values must be entered in the data source settings."), MessageHandler::Error); stop(); return; } QUrl u(QString::fromLatin1(DISCOGS_API_URL)); QUrlQuery q; switch(request().key) { case Title: u.setPath(QStringLiteral("/database/search")); q.addQueryItem(QStringLiteral("release_title"), request().value); q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); break; case Person: u.setPath(QStringLiteral("/database/search")); q.addQueryItem(QStringLiteral("artist"), request().value); q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); break; case Keyword: u.setPath(QStringLiteral("/database/search")); q.addQueryItem(QStringLiteral("q"), request().value); break; case Raw: u.setPath(QStringLiteral("/database/search")); q.setQuery(request().value); break; default: myWarning() << "key not recognized:" << request().key; stop(); return; } q.addQueryItem(QStringLiteral("token"), m_apiKey); u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); m_job->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") .arg(QStringLiteral(TELLICO_VERSION))); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &DiscogsFetcher::slotComplete); } void DiscogsFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr DiscogsFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } QString id = entry->field(QStringLiteral("discogs-id")); if(!id.isEmpty()) { // quiet QUrl u(QString::fromLatin1(DISCOGS_API_URL)); u.setPath(QStringLiteral("/releases/%1").arg(id)); QByteArray data = FileHandler::readDataFile(u, true); #if 0 myWarning() << "Remove debug2 from discogsfetcher.cpp (/tmp/test2.json)"; QFile f(QString::fromLatin1("/tmp/test2.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if(error.error == QJsonParseError::NoError) { populateEntry(entry, doc.object().toVariantMap(), true); } else { myDebug() << "Bad JSON results"; } } const QString image_id = entry->field(QStringLiteral("cover")); // if it's still a url, we need to load it if(image_id.contains(QLatin1Char('/'))) { const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); if(id.isEmpty()) { myDebug() << "empty id for" << image_id; message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } // empty image ID is ok entry->setField(QStringLiteral("cover"), id); } // don't want to include ID field entry->setField(QStringLiteral("discogs-id"), QString()); return entry; } Tellico::Fetch::FetchRequest DiscogsFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } QString artist = entry_->field(QStringLiteral("artist")); if(!artist.isEmpty()) { return FetchRequest(Person, artist); } return FetchRequest(); } void DiscogsFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from discogsfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif Data::CollPtr coll(new Data::MusicCollection(true)); // always add ID for fetchEntryHook Data::FieldPtr field(new Data::Field(QStringLiteral("discogs-id"), QStringLiteral("Discogs ID"), Data::Field::Line)); field->setCategory(i18n("General")); coll->addField(field); if(optionalFields().contains(QStringLiteral("discogs"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("discogs"), i18n("Discogs Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } if(optionalFields().contains(QStringLiteral("nationality"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("nationality"), i18n("Nationality"))); field->setCategory(i18n("General")); field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); field->setFormatType(FieldFormat::FormatPlain); coll->addField(field); } if(optionalFields().contains(QStringLiteral("producer"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("producer"), i18n("Producer"))); field->setCategory(i18n("General")); field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); field->setFormatType(FieldFormat::FormatName); coll->addField(field); } QJsonDocument doc = QJsonDocument::fromJson(data); // const QVariantMap resultMap = doc.object().toVariantMap().value(QStringLiteral("feed")).toMap(); const QVariantMap resultMap = doc.object().toVariantMap(); if(mapValue(resultMap, "message").startsWith(QLatin1String("Invalid consumer token"))) { message(i18n("The Discogs.com server reports a token error."), MessageHandler::Error); stop(); return; } int count = 0; foreach(const QVariant& result, resultMap.value(QLatin1String("results")).toList()) { if(count >= DISCOGS_MAX_RETURNS_TOTAL) { break; } // myDebug() << "found result:" << result; Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, result.toMap(), false); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); ++count; } stop(); } void DiscogsFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_, bool fullData_) { entry_->setField(QStringLiteral("discogs-id"), mapValue(resultMap_, "id")); entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry_->setField(QStringLiteral("year"), mapValue(resultMap_, "year")); entry_->setField(QStringLiteral("genre"), mapValue(resultMap_, "genres")); QStringList artists; foreach(const QVariant& artist, resultMap_.value(QLatin1String("artists")).toList()) { artists << mapValue(artist.toMap(), "name"); } entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString())); QStringList labels; foreach(const QVariant& label, resultMap_.value(QLatin1String("labels")).toList()) { labels << mapValue(label.toMap(), "name"); } entry_->setField(QStringLiteral("label"), labels.join(FieldFormat::delimiterString())); /* thumb value is not always in the full data, so go ahead and set it now */ QString coverUrl = mapValue(resultMap_, "thumb"); if(!coverUrl.isEmpty()) { entry_->setField(QStringLiteral("cover"), coverUrl); } // if we only need cursory data, then we're done if(!fullData_) { return; } // check the formats, it could have multiple // if there is a CD, prefer that in the track list bool hasCD = false; foreach(const QVariant& format, resultMap_.value(QLatin1String("formats")).toList()) { if(mapValue(format.toMap(), "name") == QLatin1String("CD")) { entry_->setField(QStringLiteral("medium"), i18n("Compact Disc")); hasCD = true; } else if(mapValue(format.toMap(), "name") == QLatin1String("Vinyl")) { entry_->setField(QStringLiteral("medium"), i18n("Vinyl")); } else if(mapValue(format.toMap(), "name") == QLatin1String("Cassette")) { entry_->setField(QStringLiteral("medium"), i18n("Cassette")); } else if(!hasCD && mapValue(format.toMap(), "name") == QLatin1String("DVD")) { // sometimes a CD and DVD both are included. If we're using the CD, ignore the DVD entry_->setField(QStringLiteral("medium"), i18n("DVD")); } } QStringList tracks; foreach(const QVariant& track, resultMap_.value(QLatin1String("tracklist")).toList()) { const QVariantMap trackMap = track.toMap(); if(mapValue(trackMap, "type_") != QLatin1String("track")) { continue; } // Releases might include a CD and a DVD, for example // prefer only the tracks on the CD. Allow positions of just numbers if(hasCD && !(mapValue(trackMap, "position").at(0).isNumber() || mapValue(trackMap, "position").startsWith(QLatin1String("CD")))) { continue; } QStringList trackInfo; trackInfo << mapValue(trackMap, "title"); if(trackMap.contains(QStringLiteral("artists"))) { QStringList artists; foreach(const QVariant& artist, trackMap.value(QLatin1String("artists")).toList()) { artists << mapValue(artist.toMap(), "name"); } trackInfo << artists.join(FieldFormat::delimiterString()); } else { trackInfo << entry_->field(QStringLiteral("artist")); } trackInfo << mapValue(trackMap, "duration"); tracks << trackInfo.join(FieldFormat::columnDelimiterString()); } entry_->setField(QStringLiteral("track"), tracks.join(FieldFormat::rowDelimiterString())); if(entry_->collection()->hasField(QStringLiteral("discogs"))) { entry_->setField(QStringLiteral("discogs"), mapValue(resultMap_, "uri")); } if(entry_->collection()->hasField(QStringLiteral("nationality"))) { entry_->setField(QStringLiteral("nationality"), mapValue(resultMap_, "country")); } if(entry_->collection()->hasField(QStringLiteral("producer"))) { QStringList producers; foreach(const QVariant& extraartist, resultMap_.value(QLatin1String("extraartists")).toList()) { if(mapValue(extraartist.toMap(), "role").contains(QStringLiteral("Producer"))) { producers << mapValue(extraartist.toMap(), "name"); } } entry_->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); } entry_->setField(QStringLiteral("comments"), mapValue(resultMap_, "notes")); } Tellico::Fetch::ConfigWidget* DiscogsFetcher::configWidget(QWidget* parent_) const { return new DiscogsFetcher::ConfigWidget(parent_, this); } QString DiscogsFetcher::defaultName() { return i18n("Discogs Audio Search"); } QString DiscogsFetcher::defaultIcon() { return favIcon("http://www.discogs.com"); } Tellico::StringHash DiscogsFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("producer")] = i18n("Producer"); hash[QStringLiteral("nationality")] = i18n("Nationality"); hash[QStringLiteral("discogs")] = i18n("Discogs Link"); return hash; } DiscogsFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const DiscogsFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("https://www.discogs.com/developers/#page:authentication")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("User token: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); if(fetcher_) { m_apiKeyEdit->setText(fetcher_->m_apiKey); } // now add additional fields widget addFieldsWidget(DiscogsFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void DiscogsFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString DiscogsFetcher::ConfigWidget::preferredName() const { return DiscogsFetcher::defaultName(); } diff --git a/src/fetch/doubanfetcher.cpp b/src/fetch/doubanfetcher.cpp index 72e52633..b50f892b 100644 --- a/src/fetch/doubanfetcher.cpp +++ b/src/fetch/doubanfetcher.cpp @@ -1,529 +1,529 @@ /*************************************************************************** Copyright (C) 2011 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 "doubanfetcher.h" #include "../collections/bookcollection.h" #include "../collections/videocollection.h" #include "../collections/musiccollection.h" #include "../images/imagefactory.h" #include "../core/filehandler.h" #include "../utils/guiproxy.h" #include "../utils/isbnvalidator.h" #include "../utils/string_utils.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int DOUBAN_MAX_RETURNS_TOTAL = 20; static const char* DOUBAN_API_URL = "https://api.douban.com/v2/"; // old and unused //static const char* DOUBAN_API_KEY = "0bd1672394eb1ebf2374356abec15c3d"; } using namespace Tellico; using Tellico::Fetch::DoubanFetcher; DoubanFetcher::DoubanFetcher(QObject* parent_) : Fetcher(parent_) , m_started(false) { } DoubanFetcher::~DoubanFetcher() { } QString DoubanFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool DoubanFetcher::canSearch(FetchKey k) const { return k == Keyword || k == ISBN; } bool DoubanFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex || type == Data::Collection::Video || type == Data::Collection::Album; } void DoubanFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void DoubanFetcher::search() { m_started = true; QUrl u(QString::fromLatin1(DOUBAN_API_URL)); switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: u.setPath(u.path() + QLatin1String("book/")); break; case Data::Collection::Video: u.setPath(u.path() + QLatin1String("movie/")); break; case Data::Collection::Album: u.setPath(u.path() + QLatin1String("music/")); break; default: myWarning() << "bad collection type:" << request().collectionType; } QUrlQuery q; switch(request().key) { case ISBN: u.setPath(u.path() + QLatin1String("isbn/")); { QStringList isbns = FieldFormat::splitValue(request().value); if(!isbns.isEmpty()) { u.setPath(u.path() + ISBNValidator::cleanValue(isbns.front())); } } break; case Keyword: u.setPath(u.path() + QLatin1String("search")); q.addQueryItem(QStringLiteral("q"), request().value); q.addQueryItem(QStringLiteral("count"), QString::number(DOUBAN_MAX_RETURNS_TOTAL)); break; default: myWarning() << "key not recognized:" << request().key; } // q.addQueryItem(QLatin1String("start"), QString::number(0)); u.setQuery(q); // myDebug() << "url:" << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); if(request().key == ISBN) { - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotCompleteISBN(KJob*))); + connect(m_job.data(), &KJob::result, this, &DoubanFetcher::slotCompleteISBN); } else { - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &DoubanFetcher::slotComplete); } } void DoubanFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void DoubanFetcher::slotCompleteISBN(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; QJsonDocument doc = QJsonDocument::fromJson(data); const QVariantMap resultMap = doc.object().toVariantMap(); // code == 6000 for no result if(mapValue(resultMap, "code") == QLatin1String("6000")) { message(mapValue(resultMap, "msg"), MessageHandler::Error); } else { Data::EntryPtr entry = createEntry(resultMap); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); m_matches.insert(r->uid, QUrl(mapValue(resultMap, "url"))); emit signalResultFound(r); } stop(); } void DoubanFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from doubanfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test-douban1.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); const QVariantMap resultsMap = doc.object().toVariantMap(); switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: foreach(const QVariant& v, resultsMap.value(QLatin1String("books")).toList()) { const QVariantMap resultMap = v.toMap(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), mapValue(resultMap, "title"), mapValue(resultMap, "author") + QLatin1Char('/') + mapValue(resultMap, "publisher") + QLatin1Char('/') + mapValue(resultMap, "pubdate").left(4)); m_matches.insert(r->uid, QUrl(mapValue(resultMap, "url"))); emit signalResultFound(r); } break; case Data::Collection::Video: foreach(const QVariant& v, resultsMap.value(QLatin1String("subjects")).toList()) { const QVariantMap resultMap = v.toMap(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), mapValue(resultMap, "title"), mapValue(resultMap, "directors", "name") + QLatin1Char('/') + mapValue(resultMap, "year")); // movie results don't appear to have a url field m_matches.insert(r->uid, QUrl(QLatin1String(DOUBAN_API_URL) + QLatin1String("movie/subject/") + mapValue(resultMap, "id"))); emit signalResultFound(r); } break; case Data::Collection::Album: foreach(const QVariant& v, resultsMap.value(QLatin1String("musics")).toList()) { const QVariantMap resultMap = v.toMap(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), mapValue(resultMap, "title"), mapValue(resultMap, "attrs", "singer") + QLatin1Char('/') + mapValue(resultMap, "attrs", "publisher") + QLatin1Char('/') + mapValue(resultMap, "attrs", "pubdate").left(4)); // movie results don't appear to have a url field m_matches.insert(r->uid, QUrl(QLatin1String(DOUBAN_API_URL) + QLatin1String("music/") + mapValue(resultMap, "id"))); emit signalResultFound(r); } break; default: break; } stop(); } Tellico::Data::EntryPtr DoubanFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(entry) { return entry; } QUrl url = m_matches.value(uid_); QByteArray data = FileHandler::readDataFile(url, true); #if 0 myWarning() << "Remove output debug from doubanfetcher.cpp"; QFile f(QLatin1String("/tmp/test-douban2.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); entry = createEntry(doc.object().toVariantMap()); if(entry) { m_entries.insert(uid_, entry); } return entry; } Tellico::Data::EntryPtr DoubanFetcher::createEntry(const QVariantMap& resultMap_) { Data::CollPtr coll; Data::EntryPtr entry; switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: coll = new Data::BookCollection(true); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "origin_title").isEmpty() && !coll->hasField(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } if(optionalFields().contains(QStringLiteral("douban")) && !mapValue(resultMap_, "alt").isEmpty() && !coll->hasField(QStringLiteral("douban"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("douban"), i18n("Douban Link"), Data::Field::URL)); f->setCategory(i18n("General")); coll->addField(f); } entry = new Data::Entry(coll); populateBookEntry(entry, resultMap_); break; case Data::Collection::Video: coll = new Data::VideoCollection(true); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty() && !coll->hasField(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } if(optionalFields().contains(QStringLiteral("douban")) && !mapValue(resultMap_, "alt").isEmpty() && !coll->hasField(QStringLiteral("douban"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("douban"), i18n("Douban Link"), Data::Field::URL)); f->setCategory(i18n("General")); coll->addField(f); } entry = new Data::Entry(coll); populateVideoEntry(entry, resultMap_); break; case Data::Collection::Album: coll = new Data::MusicCollection(true); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty() && !coll->hasField(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } if(optionalFields().contains(QStringLiteral("douban")) && !mapValue(resultMap_, "alt").isEmpty() && !coll->hasField(QStringLiteral("douban"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("douban"), i18n("Douban Link"), Data::Field::URL)); f->setCategory(i18n("General")); coll->addField(f); } entry = new Data::Entry(coll); populateMusicEntry(entry, resultMap_); break; default: break; } return entry; } void DoubanFetcher::populateBookEntry(Data::EntryPtr entry, const QVariantMap& resultMap_) { entry->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry->setField(QStringLiteral("subtitle"), mapValue(resultMap_, "subtitle")); entry->setField(QStringLiteral("author"), mapValue(resultMap_, "author")); entry->setField(QStringLiteral("translator"), mapValue(resultMap_, "translator")); entry->setField(QStringLiteral("publisher"), mapValue(resultMap_, "publisher")); const QString binding = mapValue(resultMap_, "binding"); if(binding == QStringLiteral("精装")) { entry->setField(QStringLiteral("binding"), i18n("Hardback")); } else if(binding == QStringLiteral("平装")) { entry->setField(QStringLiteral("binding"), i18n("Paperback")); } entry->setField(QStringLiteral("pub_year"), mapValue(resultMap_, "pubdate").left(4)); entry->setField(QStringLiteral("isbn"), mapValue(resultMap_, "isbn10")); entry->setField(QStringLiteral("pages"), mapValue(resultMap_, "pages")); entry->setField(QStringLiteral("cover"), mapValue(resultMap_, "image")); entry->setField(QStringLiteral("keyword"), mapValue(resultMap_, "tags", "title")); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "origin_title").isEmpty()) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap_, "origin_title")); } if(optionalFields().contains(QStringLiteral("douban"))) { entry->setField(QStringLiteral("douban"), mapValue(resultMap_, "alt")); } entry->setField(QStringLiteral("plot"), mapValue(resultMap_, "summary")); } void DoubanFetcher::populateVideoEntry(Data::EntryPtr entry, const QVariantMap& resultMap_) { entry->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry->setField(QStringLiteral("genre"), mapValue(resultMap_, "genres")); entry->setField(QStringLiteral("director"), mapValue(resultMap_, "directors", "name")); entry->setField(QStringLiteral("writer"), mapValue(resultMap_, "writers", "name")); entry->setField(QStringLiteral("year"), mapValue(resultMap_, "year")); entry->setField(QStringLiteral("cover"), mapValue(resultMap_, "images", "medium")); entry->setField(QStringLiteral("plot"), mapValue(resultMap_, "summary")); QStringList actors; foreach(const QVariant& v, resultMap_.value(QLatin1String("casts")).toList()) { actors << v.toMap().value(QStringLiteral("name")).toString(); } entry->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString())); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty()) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap_, "original_title")); } if(optionalFields().contains(QStringLiteral("douban"))) { entry->setField(QStringLiteral("douban"), mapValue(resultMap_, "alt")); } } void DoubanFetcher::populateMusicEntry(Data::EntryPtr entry, const QVariantMap& resultMap_) { entry->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry->setField(QStringLiteral("cover"), mapValue(resultMap_, "image")); entry->setField(QStringLiteral("artist"), mapValue(resultMap_, "attrs", "singer")); entry->setField(QStringLiteral("label"), mapValue(resultMap_, "attrs", "publisher")); entry->setField(QStringLiteral("year"), mapValue(resultMap_, "attrs", "pubdate").left(4)); if(mapValue(resultMap_, "attrs", "media") == QLatin1String("Audio CD") || mapValue(resultMap_, "attrs", "media") == QLatin1String("CD")) { entry->setField(QStringLiteral("medium"), i18n("Compact Disc")); } QStringList values, tracks; foreach(const QVariant& v, resultMap_.value(QLatin1String("attrs")) .toMap().value(QLatin1String("tracks")).toList()) { // some cases have all the tracks in one item, separated by "\n" and using 01. track numbers if(v.toString().contains(QLatin1Char('\n'))) { values << v.toString().split(QStringLiteral("\n")); } else { values << v.toString(); } } QRegExp trackNumRx(QLatin1String("^\\d+[.\\s]{2}")); QRegExp trackDurRx(QLatin1String("\\d+:\\d{2}")); foreach(QString value, values) { // can't be const // might starts with track number QStringList l = value.remove(trackNumRx).split(QStringLiteral(" - ")); if(l.size() == 1) { // might be split by tab characters and have track length at end l = value.remove(trackNumRx).split(QRegExp(QLatin1String("[\t\n]+"))); if(trackDurRx.exactMatch(l.last())) { tracks << l.first() + FieldFormat::columnDelimiterString() + entry->field(QStringLiteral("artist")) + FieldFormat::columnDelimiterString() + l.last(); } else { tracks << l.first(); } } else if(l.size() > 1) { const QString last = l.takeLast(); tracks << l.join(QLatin1String(" - ")) + FieldFormat::columnDelimiterString() + last; } } entry->setField(QStringLiteral("track"), tracks.join(FieldFormat::rowDelimiterString())); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty()) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap_, "original_title")); } if(optionalFields().contains(QStringLiteral("douban"))) { entry->setField(QStringLiteral("douban"), mapValue(resultMap_, "alt")); } } Tellico::Fetch::FetchRequest DoubanFetcher::updateRequest(Data::EntryPtr entry_) { QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(ISBN, isbn); } QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* DoubanFetcher::configWidget(QWidget* parent_) const { return new DoubanFetcher::ConfigWidget(parent_, this); } QString DoubanFetcher::defaultName() { return QStringLiteral("Douban.com"); } QString DoubanFetcher::defaultIcon() { return favIcon("http://www.douban.com"); } Tellico::StringHash DoubanFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("origtitle")] = i18n("Original Title"); hash[QStringLiteral("douban")] = i18n("Douban Link"); return hash; } DoubanFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const DoubanFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(DoubanFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void DoubanFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { Q_UNUSED(config_); } QString DoubanFetcher::ConfigWidget::preferredName() const { return DoubanFetcher::defaultName(); } diff --git a/src/fetch/entrezfetcher.cpp b/src/fetch/entrezfetcher.cpp index e3e66ab2..aa4704e1 100644 --- a/src/fetch/entrezfetcher.cpp +++ b/src/fetch/entrezfetcher.cpp @@ -1,488 +1,488 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "entrezfetcher.h" #include "../utils/guiproxy.h" #include "../collection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/datafileregistry.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace { static const int ENTREZ_MAX_RETURNS_TOTAL = 25; static const char* ENTREZ_BASE_URL = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/"; static const char* ENTREZ_SEARCH_CGI = "esearch.fcgi"; static const char* ENTREZ_SUMMARY_CGI = "esummary.fcgi"; static const char* ENTREZ_FETCH_CGI = "efetch.fcgi"; static const char* ENTREZ_LINK_CGI = "elink.fcgi"; static const char* ENTREZ_DEFAULT_DATABASE = "pubmed"; } using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::EntrezFetcher; EntrezFetcher::EntrezFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_start(1), m_total(-1), m_step(Begin), m_started(false) { } EntrezFetcher::~EntrezFetcher() { } QString EntrezFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool EntrezFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword || k == Raw || k == PubmedID || k == DOI; } bool EntrezFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void EntrezFetcher::readConfigHook(const KConfigGroup& config_) { QString s = config_.readEntry("Database", ENTREZ_DEFAULT_DATABASE); // default to pubmed if(!s.isEmpty()) { m_dbname = s; } } void EntrezFetcher::search() { m_started = true; m_start = 1; m_total = -1; if(m_dbname.isEmpty()) { m_dbname = QLatin1String(ENTREZ_DEFAULT_DATABASE); } QUrl u(QString::fromLatin1(ENTREZ_BASE_URL)); u.setPath(u.path() + QLatin1String(ENTREZ_SEARCH_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("retmode"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("usehistory"), QStringLiteral("y")); q.addQueryItem(QStringLiteral("retmax"), QStringLiteral("1")); // we're just getting the count q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("term"), request().value); switch(request().key) { case Title: q.addQueryItem(QStringLiteral("field"), QStringLiteral("titl")); break; case Person: q.addQueryItem(QStringLiteral("field"), QStringLiteral("auth")); break; case Keyword: // for Tellico Keyword searches basically mean search for any field matching // q.addQueryItem(QLatin1String("field"), QLatin1String("word")); break; case PubmedID: q.addQueryItem(QStringLiteral("field"), QStringLiteral("pmid")); break; case DOI: case Raw: // for DOI, enough to match any field to DOI value //q.setQuery(u.query() + QLatin1Char('&') + request().value); break; default: myWarning() << "key not supported:" << request().key; stop(); return; } u.setQuery(q); m_step = Search; // myLog() << "search url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &EntrezFetcher::slotComplete); } void EntrezFetcher::continueSearch() { m_started = true; doSummary(); } void EntrezFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; m_step = Begin; emit signalDone(this); } void EntrezFetcher::slotComplete(KJob*) { Q_ASSERT(m_job); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from entrezfetcher.cpp: " << __LINE__; QFile f(QLatin1String("/tmp/test.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif switch(m_step) { case Search: searchResults(data); break; case Summary: summaryResults(data); break; case Begin: case Fetch: default: myLog() << "wrong step =" << m_step; stop(); break; } } void EntrezFetcher::searchResults(const QByteArray& data_) { QDomDocument dom; if(!dom.setContent(data_, false)) { myWarning() << "server did not return valid XML."; stop(); return; } // find Count, QueryKey, and WebEnv elements int count = 0; for(QDomNode n = dom.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if(e.isNull()) { continue; } if(e.tagName() == QLatin1String("Count")) { m_total = e.text().toInt(); ++count; } else if(e.tagName() == QLatin1String("QueryKey")) { m_queryKey = e.text(); ++count; } else if(e.tagName() == QLatin1String("WebEnv")) { m_webEnv = e.text(); ++count; } if(count >= 3) { break; // found them all } } doSummary(); } void EntrezFetcher::doSummary() { QUrl u(QString::fromLatin1(ENTREZ_BASE_URL)); u.setPath(u.path() + QLatin1String(ENTREZ_SUMMARY_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("retmode"), QStringLiteral("xml")); if(m_start > 1) { q.addQueryItem(QStringLiteral("retstart"), QString::number(m_start)); } q.addQueryItem(QStringLiteral("retmax"), QString::number(qMin(m_total-m_start-1, ENTREZ_MAX_RETURNS_TOTAL))); q.addQueryItem(QStringLiteral("usehistory"), QStringLiteral("y")); q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("query_key"), m_queryKey); q.addQueryItem(QStringLiteral("WebEnv"), m_webEnv); u.setQuery(q); m_step = Summary; // myLog() << "summary url:" << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &EntrezFetcher::slotComplete); } void EntrezFetcher::summaryResults(const QByteArray& data_) { QDomDocument dom; if(!dom.setContent(data_, false)) { myWarning() << "server did not return valid XML."; stop(); return; } // top child is eSummaryResult // all children are DocSum for(QDomNode n = dom.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if(e.isNull() || e.tagName() != QLatin1String("DocSum")) { continue; } QDomNodeList nodes = e.elementsByTagName(QStringLiteral("Id")); if(nodes.count() == 0) { myDebug() << "no Id elements"; continue; } int id = nodes.item(0).toElement().text().toInt(); QString title, pubdate, authors; nodes = e.elementsByTagName(QStringLiteral("Item")); for(int j = 0; j < nodes.count(); ++j) { if(nodes.item(j).toElement().attribute(QStringLiteral("Name")) == QLatin1String("Title")) { title = nodes.item(j).toElement().text(); } else if(nodes.item(j).toElement().attribute(QStringLiteral("Name")) == QLatin1String("PubDate")) { pubdate = nodes.item(j).toElement().text(); } else if(nodes.item(j).toElement().attribute(QStringLiteral("Name")) == QLatin1String("AuthorList")) { QStringList list; for(QDomNode aNode = nodes.item(j).firstChild(); !aNode.isNull(); aNode = aNode.nextSibling()) { // lazy, assume all children Items are authors if(aNode.nodeName() == QLatin1String("Item")) { list << aNode.toElement().text(); } } authors = list.join(FieldFormat::delimiterString()); } if(!title.isEmpty() && !pubdate.isEmpty() && !authors.isEmpty()) { break; // done now } } FetchResult* r = new FetchResult(Fetcher::Ptr(this), title, pubdate + QLatin1Char('/') + authors); m_matches.insert(r->uid, id); emit signalResultFound(r); } m_start = m_matches.count() + 1; m_hasMoreResults = m_start <= m_total; stop(); // done searching } Tellico::Data::EntryPtr EntrezFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } if(!m_matches.contains(uid_)) { return Data::EntryPtr(); } if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return Data::EntryPtr(); } } int id = m_matches[uid_]; QUrl u(QString::fromLatin1(ENTREZ_BASE_URL)); u.setPath(u.path() + QLatin1String(ENTREZ_FETCH_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("retmode"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("rettype"), QStringLiteral("abstract")); q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("id"), QString::number(id)); u.setQuery(q); // now it's synchronous QString xmlOutput = FileHandler::readXMLFile(u, true /*quiet*/); if(xmlOutput.isEmpty()) { myWarning() << "unable to download " << u; return Data::EntryPtr(); } #if 0 myWarning() << "turn me off in entrezfetcher.cpp!"; QFile f1(QLatin1String("/tmp/test-entry.xml")); if(f1.open(QIODevice::WriteOnly)) { QTextStream t(&f1); t.setCodec("UTF-8"); t << xmlOutput; } f1.close(); #endif QString str = m_xsltHandler->applyStylesheet(xmlOutput); Import::TellicoImporter imp(str); Data::CollPtr coll = imp.collection(); if(!coll) { myWarning() << "invalid collection"; return Data::EntryPtr(); } if(coll->entryCount() == 0) { myDebug() << "no entries in collection"; return Data::EntryPtr(); } else if(coll->entryCount() > 1) { myDebug() << "collection has multiple entries, taking first one"; } Data::EntryPtr e = coll->entries().front(); // try to get a link, but only if necessary if(optionalFields().contains(QStringLiteral("url"))) { QUrl link(QString::fromLatin1(ENTREZ_BASE_URL)); link.setPath(link.path() + QLatin1String(ENTREZ_LINK_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("cmd"), QStringLiteral("llinks")); q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("dbfrom"), m_dbname); q.addQueryItem(QStringLiteral("id"), QString::number(id)); link.setQuery(q); QDomDocument linkDom = FileHandler::readXMLDocument(link, false /* namespace */, true /* quiet */); // need eLinkResult/LinkSet/IdUrlList/IdUrlSet/ObjUrl/Url QDomNode linkNode = linkDom.namedItem(QStringLiteral("eLinkResult")) .namedItem(QStringLiteral("LinkSet")) .namedItem(QStringLiteral("IdUrlList")) .namedItem(QStringLiteral("IdUrlSet")) .namedItem(QStringLiteral("ObjUrl")) .namedItem(QStringLiteral("Url")); if(!linkNode.isNull()) { QString u = linkNode.toElement().text(); // myDebug() << u; if(!u.isEmpty()) { if(!coll->hasField(QStringLiteral("url"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("url"), i18n("URL"), Data::Field::URL)); field->setCategory(i18n("Miscellaneous")); coll->addField(field); } e->setField(QStringLiteral("url"), u); } } } m_entries.insert(uid_, e); return e; } void EntrezFetcher::initXSLTHandler() { QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("pubmed2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate pubmed2tellico.xsl."; return; } QUrl u = QUrl::fromLocalFile(xsltfile); if(!m_xsltHandler) { m_xsltHandler = new XSLTHandler(u); } if(!m_xsltHandler->isValid()) { myWarning() << "error in pubmed2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } Tellico::Fetch::FetchRequest EntrezFetcher::updateRequest(Data::EntryPtr entry_) { // myDebug(); QString s = entry_->field(QStringLiteral("pmid")); if(!s.isEmpty()) { return FetchRequest(PubmedID, s); } s = entry_->field(QStringLiteral("doi")); if(!s.isEmpty()) { return FetchRequest(DOI, s); } s = entry_->field(QStringLiteral("title")); if(!s.isEmpty()) { return FetchRequest(Title, s); } return FetchRequest(); } QString EntrezFetcher::defaultName() { return i18n("Entrez Database"); } QString EntrezFetcher::defaultIcon() { return favIcon("http://www.ncbi.nlm.nih.gov"); } //static Tellico::StringHash EntrezFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("institution")] = i18n("Institution"); hash[QStringLiteral("abstract")] = i18n("Abstract"); hash[QStringLiteral("url")] = i18n("URL"); return hash; } Tellico::Fetch::ConfigWidget* EntrezFetcher::configWidget(QWidget* parent_) const { return new EntrezFetcher::ConfigWidget(parent_, this); } EntrezFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const EntrezFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(EntrezFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString EntrezFetcher::ConfigWidget::preferredName() const { return EntrezFetcher::defaultName(); } diff --git a/src/fetch/execexternalfetcher.cpp b/src/fetch/execexternalfetcher.cpp index 541f4bf1..a2de8ae5 100644 --- a/src/fetch/execexternalfetcher.cpp +++ b/src/fetch/execexternalfetcher.cpp @@ -1,550 +1,551 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "execexternalfetcher.h" #include "fetchmanager.h" #include "../collection.h" #include "../entry.h" #include "../fieldformat.h" #include "../derivedvalue.h" #include "../tellico_debug.h" #include "../gui/combobox.h" #include "../gui/lineedit.h" #include "../gui/collectiontypecombo.h" #include "../utils/cursorsaver.h" #include "../newstuff/manager.h" #include "../translators/translators.h" #include "../translators/tellicoimporter.h" #include "../translators/bibteximporter.h" #include "../translators/xsltimporter.h" #include "../translators/risimporter.h" #include "../utils/datafileregistry.h" #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::Fetch::ExecExternalFetcher; QStringList ExecExternalFetcher::parseArguments(const QString& str_) { // matching escaped quotes is too hard... :( // QRegExp quotes(QLatin1String("[^\\\\](['\"])(.*[^\\\\])\\1")); QRegExp quotes(QLatin1String("(['\"])(.*)\\1")); quotes.setMinimal(true); QRegExp spaces(QLatin1String("\\s+")); spaces.setMinimal(true); QStringList args; int pos = 0; for(int nextPos = quotes.indexIn(str_); nextPos > -1; pos = nextPos+1, nextPos = quotes.indexIn(str_, pos)) { // a non-quotes arguments runs from pos to nextPos args += str_.mid(pos, nextPos-pos).split(spaces, QString::SkipEmptyParts); // move nextpos marker to end of match pos = quotes.pos(2); // skip quotation mark nextPos += quotes.matchedLength(); args += str_.mid(pos, nextPos-pos-1); } // catch the end stuff args += str_.mid(pos).split(spaces, QString::SkipEmptyParts); return args; } ExecExternalFetcher::ExecExternalFetcher(QObject* parent_) : Fetcher(parent_), m_started(false), m_collType(-1), m_formatType(-1), m_canUpdate(false), m_process(nullptr), m_deleteOnRemove(false) { } ExecExternalFetcher::~ExecExternalFetcher() { stop(); } QString ExecExternalFetcher::source() const { return m_name; } bool ExecExternalFetcher::canFetch(int type_) const { return m_collType == -1 ? false : m_collType == type_; } void ExecExternalFetcher::readConfigHook(const KConfigGroup& config_) { QString s = config_.readPathEntry("ExecPath", QString()); if(!s.isEmpty()) { m_path = s; } QList argKeys; if(config_.hasKey("ArgumentKeys")) { argKeys = config_.readEntry("ArgumentKeys", argKeys); } else { myDebug() << "appending default keyword argument"; argKeys.append(Keyword); } QStringList args = config_.readEntry("Arguments", QStringList()); if(argKeys.count() != args.count()) { myWarning() << "unequal number of arguments and keys"; } int n = qMin(argKeys.count(), args.count()); for(int i = 0; i < n; ++i) { m_args.insert(static_cast(argKeys[i]), args[i]); } if(config_.hasKey("UpdateArgs")) { m_canUpdate = true; m_updateArgs = config_.readEntry("UpdateArgs"); } else { m_canUpdate = false; } m_collType = config_.readEntry("CollectionType", -1); m_formatType = config_.readEntry("FormatType", -1); m_deleteOnRemove = config_.readEntry("DeleteOnRemove", false); m_newStuffName = config_.readEntry("NewStuffName"); } void ExecExternalFetcher::search() { m_started = true; if(request().key != ExecUpdate && !m_args.contains(request().key)) { myDebug() << "stopping: not an update and no matching argument for search key"; stop(); return; } if(request().key == ExecUpdate) { // because the rowDelimiterString() is used below QStringList args = FieldFormat::splitTable(request().value); startSearch(args); return; } // should KShell::quoteArg() be used? // %1 gets replaced by the search value, but since the arguments are going to be split // the search value needs to be enclosed in quotation marks // but first check to make sure the user didn't do that already // AND the "%1" wasn't used in the settings QString value = request().value; if(request().key == ISBN) { value.remove(QLatin1Char('-')); // remove hyphens from isbn values // shouldn't hurt and might keep from confusing stupid search sources } QRegExp rx1(QLatin1String("['\"].*\\1")); if(!rx1.exactMatch(value)) { value = QLatin1Char('"') + value + QLatin1Char('"'); } QString args = m_args.value(request().key); QRegExp rx2(QLatin1String("['\"]%1\\1")); args.replace(rx2, QStringLiteral("%1")); startSearch(parseArguments(args.arg(value))); // replace %1 with search value } void ExecExternalFetcher::startSearch(const QStringList& args_) { if(m_path.isEmpty()) { Q_ASSERT(!m_path.isEmpty()); stop(); return; } m_process = new KProcess(); - connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(slotData())); - connect(m_process, SIGNAL(readyReadStandardError()), SLOT(slotError())); - connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(slotProcessExited())); + connect(m_process, &QProcess::readyReadStandardOutput, this, &ExecExternalFetcher::slotData); + connect(m_process, &QProcess::readyReadStandardError, this, &ExecExternalFetcher::slotError); + void (QProcess::* finished)(int, QProcess::ExitStatus) = &QProcess::finished; + connect(m_process, finished, this, &ExecExternalFetcher::slotProcessExited); m_process->setOutputChannelMode(KProcess::SeparateChannels); m_process->setProgram(m_path, args_); if(m_process && m_process->execute() < 0) { myDebug() << "process failed to start"; stop(); } } void ExecExternalFetcher::stop() { if(!m_started) { return; } if(m_process) { m_process->kill(); m_process->deleteLater(); m_process = nullptr; } m_data.clear(); m_started = false; m_errors.clear(); emit signalDone(this); } void ExecExternalFetcher::slotData() { m_data.append(m_process->readAllStandardOutput()); } void ExecExternalFetcher::slotError() { GUI::CursorSaver cs(Qt::ArrowCursor); QString msg = QString::fromLocal8Bit(m_process->readAllStandardError()); msg.prepend(source() + QLatin1String(": ")); if(msg.endsWith(QChar::fromLatin1('\n'))) { msg.truncate(msg.length()-1); } myDebug() << msg; m_errors << msg; } void ExecExternalFetcher::slotProcessExited() { // DEBUG_LINE; if(m_process->exitStatus() != QProcess::NormalExit || m_process->exitCode() != 0) { myDebug() << source() << ": process did not exit successfully"; if(!m_errors.isEmpty()) { message(m_errors.join(QChar::fromLatin1('\n')), MessageHandler::Error); } stop(); return; } if(!m_errors.isEmpty()) { message(m_errors.join(QChar::fromLatin1('\n')), MessageHandler::Warning); } if(m_data.isEmpty()) { myDebug() << source() << ": no data"; stop(); return; } const QString text = QString::fromUtf8(m_data.constData(), m_data.size()); Import::Format format = static_cast(m_formatType > -1 ? m_formatType : Import::TellicoXML); Import::Importer* imp = nullptr; // only 4 formats re supported here switch(format) { case Import::TellicoXML: imp = new Import::TellicoImporter(text); break; case Import::Bibtex: imp = new Import::BibtexImporter(text); break; case Import::MODS: imp = new Import::XSLTImporter(text); { QString xsltFile = DataFileRegistry::self()->locate(QStringLiteral("mods2tellico.xsl")); if(!xsltFile.isEmpty()) { QUrl u = QUrl::fromLocalFile(xsltFile); static_cast(imp)->setXSLTURL(u); } else { myWarning() << "unable to find mods2tellico.xml!"; delete imp; imp = nullptr; } } break; case Import::RIS: imp = new Import::RISImporter(text); break; default: break; } if(!imp) { stop(); return; } Data::CollPtr coll = imp->collection(); if(!coll) { if(!imp->statusMessage().isEmpty()) { message(imp->statusMessage(), MessageHandler::Status); } myDebug() << source() << ": no collection pointer"; delete imp; stop(); return; } delete imp; if(coll->entryCount() == 0) { // myDebug() << "no results"; stop(); return; } Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } stop(); // be sure to call this } Tellico::Data::EntryPtr ExecExternalFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest ExecExternalFetcher::updateRequest(Data::EntryPtr entry_) { if(!m_canUpdate) { return FetchRequest(); } QStringList args = parseArguments(m_updateArgs); for(QStringList::Iterator it = args.begin(); it != args.end(); ++it) { Data::DerivedValue dv(*it); *it = dv.value(entry_, false); } return FetchRequest(ExecUpdate, args.join(FieldFormat::rowDelimiterString())); } Tellico::Fetch::ConfigWidget* ExecExternalFetcher::configWidget(QWidget* parent_) const { return new ExecExternalFetcher::ConfigWidget(parent_, this); } QString ExecExternalFetcher::defaultName() { return i18n("External Application"); } QString ExecExternalFetcher::defaultIcon() { return QStringLiteral("application-x-executable"); } ExecExternalFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ExecExternalFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_), m_deleteOnRemove(false) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); l->addWidget(label, ++row, 0); m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); - connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_collCombo, activatedInt, this, &ConfigWidget::slotSetModified); l->addWidget(m_collCombo, row, 1); QString w = i18n("Set the collection type of the data returned from the external application."); label->setWhatsThis(w); m_collCombo->setWhatsThis(w); label->setBuddy(m_collCombo); label = new QLabel(i18n("&Result type: "), optionsWidget()); l->addWidget(label, ++row, 0); m_formatCombo = new GUI::ComboBox(optionsWidget()); m_formatCombo->addItem(QStringLiteral("Tellico"), Import::TellicoXML); m_formatCombo->addItem(QStringLiteral("Bibtex"), Import::Bibtex); m_formatCombo->addItem(QStringLiteral("MODS"), Import::MODS); m_formatCombo->addItem(QStringLiteral("RIS"), Import::RIS); - connect(m_formatCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_formatCombo, activatedInt, this, &ExecExternalFetcher::ConfigWidget::slotSetModified); l->addWidget(m_formatCombo, row, 1); w = i18n("Set the result type of the data returned from the external application."); label->setWhatsThis(w); m_formatCombo->setWhatsThis(w); label->setBuddy(m_formatCombo); label = new QLabel(i18n("Application &path: "), optionsWidget()); l->addWidget(label, ++row, 0); m_pathEdit = new KUrlRequester(optionsWidget()); - connect(m_pathEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_pathEdit, &KUrlRequester::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_pathEdit, row, 1); w = i18n("Set the path of the application to run that should output a valid Tellico data file."); label->setWhatsThis(w); m_pathEdit->setWhatsThis(w); label->setBuddy(m_pathEdit); w = i18n("Select the search keys supported by the data source."); // in this string, the %1 is not a placeholder, it's an example QString w2 = i18n("Add any arguments that may be needed. %1 will be replaced by the search term."); // krazy:exclude=i18ncheckarg QGroupBox* gbox = new QGroupBox(i18n("Arguments"), optionsWidget()); ++row; l->addWidget(gbox, row, 0, 1, 2); QGridLayout* gridLayout = new QGridLayout(gbox); gridLayout->setSpacing(2); row = -1; const Fetch::KeyMap keyMap = Fetch::Manager::self()->keyMap(); for(Fetch::KeyMap::ConstIterator it = keyMap.begin(); it != keyMap.end(); ++it) { FetchKey key = it.key(); if(key == Raw) { continue; } QCheckBox* cb = new QCheckBox(it.value(), gbox); gridLayout->addWidget(cb, ++row, 0); m_cbDict.insert(key, cb); GUI::LineEdit* le = new GUI::LineEdit(gbox); le->setPlaceholderText(QStringLiteral("%1")); // for example le->completionObject()->addItem(QStringLiteral("%1")); gridLayout->addWidget(le, row, 1); m_leDict.insert(key, le); if(fetcher_ && fetcher_->m_args.contains(key)) { cb->setChecked(true); le->setEnabled(true); le->setText(fetcher_->m_args.value(key)); } else { cb->setChecked(false); le->setEnabled(false); } - connect(cb, SIGNAL(toggled(bool)), le, SLOT(setEnabled(bool))); + connect(cb, &QAbstractButton::toggled, le, &QWidget::setEnabled); cb->setWhatsThis(w); le->setWhatsThis(w2); } m_cbUpdate = new QCheckBox(i18n("Update"), gbox); gridLayout->addWidget(m_cbUpdate, ++row, 0); m_leUpdate = new GUI::LineEdit(gbox); m_leUpdate->setPlaceholderText(QStringLiteral("%{title}")); // for example m_leUpdate->completionObject()->addItem(QStringLiteral("%{title}")); m_leUpdate->completionObject()->addItem(QStringLiteral("%{isbn}")); gridLayout->addWidget(m_leUpdate, row, 1); /* TRANSLATORS: Do not translate %{author}. */ w2 = i18n("

    Enter the arguments which should be used to search for available updates to an entry.

    " "The format is the same as for fields with derived values, where field names " "are contained inside braces, such as %{author}. See the documentation for details.

    "); m_cbUpdate->setWhatsThis(w); m_leUpdate->setWhatsThis(w2); if(fetcher_ && fetcher_->m_canUpdate) { m_cbUpdate->setChecked(true); m_leUpdate->setEnabled(true); m_leUpdate->setText(fetcher_->m_updateArgs); } else { m_cbUpdate->setChecked(false); m_leUpdate->setEnabled(false); } - connect(m_cbUpdate, SIGNAL(toggled(bool)), m_leUpdate, SLOT(setEnabled(bool))); + connect(m_cbUpdate, &QAbstractButton::toggled, m_leUpdate, &QWidget::setEnabled); l->setRowStretch(++row, 1); if(fetcher_) { m_pathEdit->setUrl(QUrl::fromLocalFile(fetcher_->m_path)); m_newStuffName = fetcher_->m_newStuffName; } if(fetcher_ && fetcher_->m_collType > -1) { m_collCombo->setCurrentType(fetcher_->m_collType); } else { m_collCombo->setCurrentType(Data::Collection::Book); } if(fetcher_ && fetcher_->m_formatType > -1) { m_formatCombo->setCurrentData(fetcher_->m_formatType); } else { m_formatCombo->setCurrentData(Import::TellicoXML); } m_deleteOnRemove = fetcher_ && fetcher_->m_deleteOnRemove; KAcceleratorManager::manage(optionsWidget()); } ExecExternalFetcher::ConfigWidget::~ConfigWidget() { } void ExecExternalFetcher::ConfigWidget::readConfig(const KConfigGroup& config_) { m_pathEdit->setUrl(QUrl::fromLocalFile(config_.readPathEntry("ExecPath", QString()))); QList argKeys = config_.readEntry("ArgumentKeys", QList()); QStringList argValues = config_.readEntry("Arguments", QStringList()); if(argKeys.count() != argValues.count()) { myWarning() << "unequal number of arguments and keys"; } int n = qMin(argKeys.count(), argValues.count()); QMap args; for(int i = 0; i < n; ++i) { args[static_cast(argKeys[i])] = argValues[i]; } for(QList::Iterator it = argKeys.begin(); it != argKeys.end(); ++it) { if(*it == Raw) { continue; } FetchKey key = static_cast(*it); QCheckBox* cb = m_cbDict[key]; QLineEdit* le = m_leDict[key]; if(cb && le) { if(args.contains(key)) { cb->setChecked(true); le->setEnabled(true); le->setText(args[key]); } else { cb->setChecked(false); le->setEnabled(false); le->clear(); } } } if(config_.hasKey("UpdateArgs")) { m_cbUpdate->setChecked(true); m_leUpdate->setEnabled(true); m_leUpdate->setText(config_.readEntry("UpdateArgs")); } else { m_cbUpdate->setChecked(false); m_leUpdate->setEnabled(false); m_leUpdate->clear(); } int collType = config_.readEntry("CollectionType", -1); m_collCombo->setCurrentType(collType); int formatType = config_.readEntry("FormatType", -1); m_formatCombo->setCurrentData(static_cast(formatType)); m_deleteOnRemove = config_.readEntry("DeleteOnRemove", false); m_name = config_.readEntry("Name"); m_newStuffName = config_.readEntry("NewStuffName"); } void ExecExternalFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QUrl u = m_pathEdit->url(); if(!u.isEmpty()) { config_.writePathEntry("ExecPath", u.path()); } QList keys; QStringList args; QHash::const_iterator it = m_cbDict.constBegin(); for( ; it != m_cbDict.constEnd(); ++it) { if(it.value()->isChecked()) { keys << it.key(); args << m_leDict[it.key()]->text(); } } config_.writeEntry("ArgumentKeys", keys); config_.writeEntry("Arguments", args); if(m_cbUpdate->isChecked()) { config_.writeEntry("UpdateArgs", m_leUpdate->text()); } else { config_.deleteEntry("UpdateArgs"); } config_.writeEntry("CollectionType", m_collCombo->currentType()); config_.writeEntry("FormatType", m_formatCombo->currentData().toInt()); config_.writeEntry("DeleteOnRemove", m_deleteOnRemove); if(!m_newStuffName.isEmpty()) { config_.writeEntry("NewStuffName", m_newStuffName); } - slotSetModified(false); } void ExecExternalFetcher::ConfigWidget::removed() { if(!m_deleteOnRemove) { return; } if(!m_newStuffName.isEmpty()) { NewStuff::Manager::self()->removeScript(m_newStuffName); } } QString ExecExternalFetcher::ConfigWidget::preferredName() const { return m_name.isEmpty() ? ExecExternalFetcher::defaultName() : m_name; } diff --git a/src/fetch/fetcherjob.cpp b/src/fetch/fetcherjob.cpp index c405ae54..b0165e58 100644 --- a/src/fetch/fetcherjob.cpp +++ b/src/fetch/fetcherjob.cpp @@ -1,95 +1,95 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "fetcherjob.h" #include "../entry.h" #include "../tellico_debug.h" #include using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::FetcherJob; FetcherJob::FetcherJob(QObject* parent_, Fetcher::Ptr fetcher_, const FetchRequest& request_) : KJob(parent_), m_fetcher(fetcher_), m_request(request_), m_maximumResults(0) { - connect(m_fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SLOT(slotResult(Tellico::Fetch::FetchResult*))); - connect(m_fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotDone())); + connect(m_fetcher.data(), &Fetcher::signalResultFound, + this, &FetcherJob::slotResult); + connect(m_fetcher.data(), &Fetcher::signalDone, + this, &FetcherJob::slotDone); } FetcherJob::~FetcherJob() { qDeleteAll(m_results); m_results.clear(); } Tellico::Data::EntryList FetcherJob::entries() { Data::EntryList list; foreach(FetchResult* result, m_results) { Data::EntryPtr entry = result->fetchEntry(); if(entry) { list << entry; } } return list; } void FetcherJob::setMaximumResults(int count_) { Q_ASSERT(count_ >= 0); m_maximumResults = count_; } void FetcherJob::start() { - QTimer::singleShot(0, this, SLOT(startSearch())); + QTimer::singleShot(0, this, &FetcherJob::startSearch); } void FetcherJob::startSearch() { m_fetcher->startSearch(m_request); } void FetcherJob::slotResult(Tellico::Fetch::FetchResult* result_) { if(!result_) { myDebug() << "null result"; return; } m_results.append(result_); if(m_maximumResults > 0 && m_results.count() >= m_maximumResults) { doKill(); } } void FetcherJob::slotDone() { // only continue if more results were specifically asked for if(m_fetcher->hasMoreResults() && m_results.count() < m_maximumResults) { m_fetcher->continueSearch(); } else { emitResult(); } } bool FetcherJob::doKill() { m_fetcher->stop(); return true; } diff --git a/src/fetch/fetchmanager.cpp b/src/fetch/fetchmanager.cpp index 9fc88758..8405796c 100644 --- a/src/fetch/fetchmanager.cpp +++ b/src/fetch/fetchmanager.cpp @@ -1,586 +1,586 @@ /*************************************************************************** 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 . * * * ***************************************************************************/ #include #include "fetchmanager.h" #include "configwidget.h" #include "messagehandler.h" #include "../entry.h" #include "../collection.h" #include "../document.h" #include "../utils/string_utils.h" #include "../utils/tellico_utils.h" #include "../tellico_debug.h" #ifdef HAVE_YAZ #include "z3950fetcher.h" #endif #include "srufetcher.h" #include "execexternalfetcher.h" #include #include #include #include #include #include #include #define LOAD_ICON(name, group, size) \ KIconLoader::global()->loadIcon(name, static_cast(group), size_) using Tellico::Fetch::Manager; Manager* Manager::s_self = nullptr; Manager::Manager() : QObject(), m_currentFetcherIndex(-1), m_messager(new ManagerMessage()), m_count(0), m_loadDefaults(false) { // must create static pointer first Q_ASSERT(!s_self); s_self = this; // no need to load fetchers since the initializer does it for us // m_keyMap.insert(FetchFirst, QString()); m_keyMap.insert(Title, i18n("Title")); m_keyMap.insert(Person, i18n("Person")); m_keyMap.insert(ISBN, i18n("ISBN")); m_keyMap.insert(UPC, i18n("UPC/EAN")); m_keyMap.insert(Keyword, i18n("Keyword")); m_keyMap.insert(DOI, i18n("DOI")); m_keyMap.insert(ArxivID, i18n("arXiv ID")); m_keyMap.insert(PubmedID, i18n("PubMed ID")); m_keyMap.insert(LCCN, i18n("LCCN")); m_keyMap.insert(Raw, i18n("Raw Query")); // m_keyMap.insert(FetchLast, QString()); } Manager::~Manager() { delete m_messager; } void Manager::registerFunction(int type_, const FetcherFunction& func_) { functionRegistry.insert(type_, func_); } void Manager::loadFetchers() { m_fetchers.clear(); m_uuidHash.clear(); KSharedConfigPtr config = KSharedConfig::openConfig(); if(config->hasGroup(QStringLiteral("Data Sources"))) { KConfigGroup configGroup(config, QStringLiteral("Data Sources")); int nSources = configGroup.readEntry("Sources Count", 0); for(int i = 0; i < nSources; ++i) { QString group = QStringLiteral("Data Source %1").arg(i); Fetcher::Ptr f = createFetcher(config, group); if(f) { m_fetchers.append(f); f->setMessageHandler(m_messager); m_uuidHash.insert(f->uuid(), f); } } m_loadDefaults = false; } else { // add default sources m_fetchers = defaultFetchers(); m_loadDefaults = true; } } const Tellico::Fetch::FetcherVec& Manager::fetchers() const { return m_fetchers; } Tellico::Fetch::FetcherVec Manager::fetchers(int type_) { FetcherVec vec; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(fetcher->canFetch(type_)) { vec.append(fetcher); } } return vec; } Tellico::Fetch::Fetcher::Ptr Manager::fetcherByUuid(const QString& uuid_) { return m_uuidHash.contains(uuid_) ? m_uuidHash[uuid_] : Fetcher::Ptr(); } Tellico::Fetch::KeyMap Manager::keyMap(const QString& source_) const { // an empty string means return all if(source_.isEmpty()) { return m_keyMap; } // assume there's only one fetcher match Fetcher::Ptr foundFetcher; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(source_ == fetcher->source()) { foundFetcher = fetcher; break; } } if(!foundFetcher) { myWarning() << "no fetcher found!"; return KeyMap(); } KeyMap map; for(KeyMap::ConstIterator it = m_keyMap.constBegin(); it != m_keyMap.constEnd(); ++it) { if(foundFetcher->canSearch(it.key())) { map.insert(it.key(), it.value()); } } return map; } void Manager::startSearch(const QString& source_, Tellico::Fetch::FetchKey key_, const QString& value_) { if(value_.isEmpty()) { emit signalDone(); return; } FetchRequest request(Data::Document::self()->collection()->type(), key_, value_); // assume there's only one fetcher match int i = 0; m_currentFetcherIndex = -1; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(source_ == fetcher->source()) { ++m_count; // Fetcher::search() might emit done(), so increment before calling search() - connect(fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*))); - connect(fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotFetcherDone(Tellico::Fetch::Fetcher*))); + connect(fetcher.data(), &Fetcher::signalResultFound, + this, &Manager::signalResultFound); + connect(fetcher.data(), &Fetcher::signalDone, + this, &Manager::slotFetcherDone); fetcher->startSearch(request); m_currentFetcherIndex = i; break; } ++i; } } void Manager::continueSearch() { if(m_currentFetcherIndex < 0 || m_currentFetcherIndex >= static_cast(m_fetchers.count())) { myDebug() << "can't continue!"; emit signalDone(); return; } Fetcher::Ptr fetcher = m_fetchers[m_currentFetcherIndex]; if(fetcher && fetcher->hasMoreResults()) { ++m_count; - connect(fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*))); - connect(fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotFetcherDone(Tellico::Fetch::Fetcher*))); + connect(fetcher.data(), &Fetcher::signalResultFound, + this, &Manager::signalResultFound); + connect(fetcher.data(), &Fetcher::signalDone, + this, &Manager::slotFetcherDone); fetcher->continueSearch(); } else { emit signalDone(); } } bool Manager::hasMoreResults() const { if(m_currentFetcherIndex < 0 || m_currentFetcherIndex >= static_cast(m_fetchers.count())) { return false; } Fetcher::Ptr fetcher = m_fetchers[m_currentFetcherIndex]; return fetcher && fetcher->hasMoreResults(); } void Manager::stop() { // DEBUG_LINE; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(fetcher->isSearching()) { fetcher->stop(); fetcher->saveConfig(); } } #ifndef NDEBUG if(m_count != 0) { myDebug() << "count should be 0!"; } #endif m_count = 0; } void Manager::slotFetcherDone(Tellico::Fetch::Fetcher* fetcher_) { // myDebug() << (fetcher_ ? fetcher_->source() : QString()) << ":" << m_count; fetcher_->disconnect(); // disconnect all signals fetcher_->saveConfig(); --m_count; if(m_count <= 0) { emit signalDone(); } } bool Manager::canFetch() const { foreach(Fetcher::Ptr fetcher, m_fetchers) { if(fetcher->canFetch(Data::Document::self()->collection()->type())) { return true; } } return false; } Tellico::Fetch::Fetcher::Ptr Manager::createFetcher(KSharedConfigPtr config_, const QString& group_) { if(!config_->hasGroup(group_)) { myDebug() << "no config group for " << group_; return Fetcher::Ptr(); } KConfigGroup config(config_, group_); int fetchType = config.readEntry("Type", int(Fetch::Unknown)); if(fetchType == Fetch::Unknown) { myDebug() << "unknown type " << fetchType << ", skipping"; return Fetcher::Ptr(); } // special case: the BoardGameGeek fetcher was originally implemented as a Ruby script // now, it's available with an XML API, so prefer the new version // so check for fetcher version and switch to the XML if version is missing or lower if(fetchType == Fetch::ExecExternal && config.readPathEntry("ExecPath", QString()).endsWith(QLatin1String("boardgamegeek.rb"))) { KConfigGroup generalConfig(config_, QStringLiteral("General Options")); if(generalConfig.readEntry("FetchVersion", 0) < 1) { fetchType = Fetch::BoardGameGeek; generalConfig.writeEntry("FetchVersion", 1); } } // special case: the Bedetheque fetcher was originally implemented as a Python script // now, it's available as a builtin data source, so prefer the new version // so check for fetcher version and switch to the newer if version is missing or lower if(fetchType == Fetch::ExecExternal && config.readPathEntry("ExecPath", QString()).endsWith(QStringLiteral("bedetheque.py"))) { KConfigGroup generalConfig(config_, QStringLiteral("General Options")); if(generalConfig.readEntry("FetchVersion", 0) < 2) { fetchType = Fetch::Bedetheque; generalConfig.writeEntry("FetchVersion", 2); } } Fetcher::Ptr f; if(functionRegistry.contains(fetchType)) { f = functionRegistry.value(fetchType).create(this); f->readConfig(config, group_); } return f; } #define FETCHER_ADD(type) \ do { \ if(functionRegistry.contains(type)) { \ vec.append(functionRegistry.value(type).create(this)); \ } \ } while(false) // static Tellico::Fetch::FetcherVec Manager::defaultFetchers() { FetcherVec vec; vec.append(SRUFetcher::libraryOfCongress(this)); // books FETCHER_ADD(ISBNdb); FETCHER_ADD(OpenLibrary); FETCHER_ADD(GoogleBook); // comic books FETCHER_ADD(AnimeNfo); FETCHER_ADD(Bedetheque); FETCHER_ADD(ComicVine); // bibliographic FETCHER_ADD(Arxiv); FETCHER_ADD(GoogleScholar); FETCHER_ADD(BiblioShare); FETCHER_ADD(DBLP); FETCHER_ADD(HathiTrust); // music FETCHER_ADD(MusicBrainz); // video games FETCHER_ADD(TheGamesDB); FETCHER_ADD(IGDB); FETCHER_ADD(VNDB); FETCHER_ADD(VideoGameGeek); // board games FETCHER_ADD(BoardGameGeek); // movies FETCHER_ADD(TheMovieDB); #ifdef ENABLE_IMDB FETCHER_ADD(IMDB); #endif QStringList langs = QLocale().uiLanguages(); if(langs.first().contains(QLatin1Char('-'))) { // I'm not sure QT always include two-letter locale codes langs << langs.first().section(QLatin1Char('-'), 0, 0); } // only add IBS if user includes italian if(langs.contains(QStringLiteral("it"))) { FETCHER_ADD(IBS); } if(langs.contains(QStringLiteral("fr"))) { FETCHER_ADD(DVDFr); FETCHER_ADD(Allocine); } if(langs.contains(QStringLiteral("ru"))) { FETCHER_ADD(KinoPoisk); } if(langs.contains(QStringLiteral("ua"))) { FETCHER_ADD(KinoTeatr); } if(langs.contains(QStringLiteral("de"))) { FETCHER_ADD(Kino); } if(langs.contains(QStringLiteral("cn"))) { FETCHER_ADD(Douban); } if(langs.contains(QStringLiteral("dk"))) { FETCHER_ADD(DBC); } return vec; } #undef FETCHER_ADD Tellico::Fetch::FetcherVec Manager::createUpdateFetchers(int collType_) { if(m_loadDefaults) { return defaultFetchers(); } FetcherVec vec; KConfigGroup config(KSharedConfig::openConfig(), "Data Sources"); int nSources = config.readEntry("Sources Count", 0); for(int i = 0; i < nSources; ++i) { QString group = QStringLiteral("Data Source %1").arg(i); // needs the KConfig* Fetcher::Ptr fetcher = createFetcher(KSharedConfig::openConfig(), group); if(fetcher && fetcher->canFetch(collType_) && fetcher->canUpdate()) { vec.append(fetcher); } } return vec; } Tellico::Fetch::FetcherVec Manager::createUpdateFetchers(int collType_, Tellico::Fetch::FetchKey key_) { FetcherVec fetchers; // creates new fetchers FetcherVec allFetchers = createUpdateFetchers(collType_); foreach(Fetcher::Ptr fetcher, allFetchers) { if(fetcher->canSearch(key_)) { fetchers.append(fetcher); } } return fetchers; } Tellico::Fetch::Fetcher::Ptr Manager::createUpdateFetcher(int collType_, const QString& source_) { Fetcher::Ptr newFetcher; // creates new fetchers FetcherVec fetchers = createUpdateFetchers(collType_); foreach(Fetcher::Ptr fetcher, fetchers) { if(fetcher->source() == source_) { newFetcher = fetcher; break; } } return newFetcher; } void Manager::updateStatus(const QString& message_) { emit signalStatus(message_); } Tellico::Fetch::NameTypeMap Manager::nameTypeMap() { Fetch::NameTypeMap map; FunctionRegistry::const_iterator it = functionRegistry.constBegin(); while(it != functionRegistry.constEnd()) { map.insert(functionRegistry.value(it.key()).name(), static_cast(it.key())); ++it; } // now find all the scripts distributed with tellico QStringList files = Tellico::locateAllFiles(QStringLiteral("tellico/data-sources/*.spec")); foreach(const QString& file, files) { KConfig spec(file, KConfig::SimpleConfig); KConfigGroup specConfig(&spec, QString()); QString name = specConfig.readEntry("Name"); if(name.isEmpty()) { myDebug() << "no name for" << file; continue; } bool enabled = specConfig.readEntry("Enabled", true); if(!enabled || !bundledScriptHasExecPath(file, specConfig)) { // no available exec continue; } map.insert(name, ExecExternal); m_scriptMap.insert(name, file); } return map; } // called when creating a new fetcher Tellico::Fetch::ConfigWidget* Manager::configWidget(QWidget* parent_, Tellico::Fetch::Type type_, const QString& name_) { ConfigWidget* w = nullptr; if(functionRegistry.contains(type_)) { w = functionRegistry.value(type_).configWidget(parent_); } else { myWarning() << "no widget defined for type =" << type_; } if(w && type_ == ExecExternal) { if(!name_.isEmpty() && m_scriptMap.contains(name_)) { // bundledScriptHasExecPath() actually needs to write the exec path // back to the config so the configWidget can read it. But if the spec file // is not readable, that doesn't work. So work around it with a copy to a temp file QTemporaryFile tmpFile; tmpFile.open(); QUrl from = QUrl::fromLocalFile(m_scriptMap[name_]); QUrl to = QUrl::fromLocalFile(tmpFile.fileName()); // have to overwrite since QTemporaryFile already created it KIO::Job* job = KIO::file_copy(from, to, -1, KIO::Overwrite); if(!job->exec()) { myDebug() << job->errorString(); } KConfig spec(to.path(), KConfig::SimpleConfig); KConfigGroup specConfig(&spec, QString()); // pass actual location of spec file if(name_ == specConfig.readEntry("Name") && bundledScriptHasExecPath(m_scriptMap[name_], specConfig)) { w->readConfig(specConfig); } else { myWarning() << "Can't read config file for " << to.path(); } } } return w; } // static QString Manager::typeName(Tellico::Fetch::Type type_) { if(self()->functionRegistry.contains(type_)) { return self()->functionRegistry.value(type_).name(); } myWarning() << "none found for" << type_; return QString(); } QPixmap Manager::fetcherIcon(Tellico::Fetch::Fetcher::Ptr fetcher_, int group_, int size_) { if(fetcher_->type() == Fetch::Z3950) { #ifdef HAVE_YAZ const Fetch::Z3950Fetcher* f = static_cast(fetcher_.data()); QUrl u; u.setScheme(QStringLiteral("http")); u.setHost(f->host()); QString icon = Fetcher::favIcon(u); if(!icon.isEmpty()) { return LOAD_ICON(icon, group_, size_); } #endif } else if(fetcher_->type() == Fetch::ExecExternal) { const Fetch::ExecExternalFetcher* f = static_cast(fetcher_.data()); const QString p = f->execPath(); QUrl u; if(p.contains(QStringLiteral("allocine"))) { u = QUrl(QStringLiteral("http://www.allocine.fr")); } else if(p.contains(QStringLiteral("ministerio_de_cultura"))) { u = QUrl(QStringLiteral("http://www.mcu.es")); } else if(p.contains(QStringLiteral("dark_horse_comics"))) { u = QUrl(QStringLiteral("http://www.darkhorse.com")); } else if(p.contains(QStringLiteral("boardgamegeek"))) { u = QUrl(QStringLiteral("http://www.boardgamegeek.com")); } else if(p.contains(QStringLiteral("supercat"))) { u = QUrl(QStringLiteral("https://evergreen-ils.org")); } else if(f->source().contains(QStringLiteral("amarok"), Qt::CaseInsensitive)) { return LOAD_ICON(QStringLiteral("amarok"), group_, size_); } if(!u.isEmpty() && u.isValid()) { QString icon = Fetcher::favIcon(u); if(!icon.isEmpty()) { return LOAD_ICON(icon, group_, size_); } } } return fetcherIcon(fetcher_->type(), group_, size_); } QPixmap Manager::fetcherIcon(Tellico::Fetch::Type type_, int group_, int size_) { QString name; if(self()->functionRegistry.contains(type_)) { name = self()->functionRegistry.value(type_).icon(); } else { myWarning() << "no pixmap defined for type =" << type_; } if(name.isEmpty()) { // use default tellico application icon name = QStringLiteral("tellico"); } QPixmap pix = KIconLoader::global()->loadIcon(name, static_cast(group_), size_, KIconLoader::DefaultState, QStringList(), nullptr, true); if(pix.isNull()) { QIcon icon = QIcon::fromTheme(name); const int groupSize = KIconLoader::global()->currentSize(static_cast(group_)); size_ = size_ == 0 ? groupSize : size_; pix = icon.pixmap(size_, size_); } if(pix.isNull()) { pix = BarIcon(name); } return pix; } Tellico::StringHash Manager::optionalFields(Type type_) { if(self()->functionRegistry.contains(type_)) { return self()->functionRegistry.value(type_).optionalFields(); } return StringHash(); } bool Manager::bundledScriptHasExecPath(const QString& specFile_, KConfigGroup& config_) { // make sure ExecPath is set and executable // for the bundled scripts, either the exec name is not set, in which case it is the // name of the spec file, minus the .spec, or the exec is set, and is local to the dir // if not, look for it QFileInfo specInfo(specFile_); QString exec = config_.readPathEntry("ExecPath", QString()); QFileInfo execInfo(exec); if(exec.isEmpty() || !execInfo.exists()) { exec = specInfo.canonicalPath() + QDir::separator() + specInfo.completeBaseName(); // remove ".spec" } else if(execInfo.isRelative()) { exec = specInfo.canonicalPath() + QDir::separator() + exec; } else if(!execInfo.isExecutable()) { myWarning() << "not executable:" << specFile_; return false; } execInfo.setFile(exec); if(!execInfo.exists() || !execInfo.isExecutable()) { myWarning() << "no exec file for" << specFile_; myWarning() << "exec =" << exec; return false; // we're not ok } config_.writePathEntry("ExecPath", exec); config_.sync(); // might be readonly, but that's ok return true; } diff --git a/src/fetch/filmasterfetcher.cpp b/src/fetch/filmasterfetcher.cpp index aed124ac..14f1df2c 100644 --- a/src/fetch/filmasterfetcher.cpp +++ b/src/fetch/filmasterfetcher.cpp @@ -1,329 +1,329 @@ /*************************************************************************** Copyright (C) 2011 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 "filmasterfetcher.h" #include "../collections/videocollection.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../entry.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* FILMASTER_API_URL = "http://api.filmaster.com"; static const char* FILMASTER_QUERY_URL = "http://api.filmaster.com/1.1/search/"; } using namespace Tellico; using Tellico::Fetch::FilmasterFetcher; FilmasterFetcher::FilmasterFetcher(QObject* parent_) : Fetcher(parent_), m_started(false) { } FilmasterFetcher::~FilmasterFetcher() { } QString FilmasterFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString FilmasterFetcher::attribution() const { return i18n("This data is licensed under specific terms.", QLatin1String("http://filmaster.com/license/")); } bool FilmasterFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword; } bool FilmasterFetcher::canFetch(int type) const { return type == Data::Collection::Video; } void FilmasterFetcher::readConfigHook(const KConfigGroup&) { } void FilmasterFetcher::search() { m_started = true; QUrl u(QString::fromLatin1(FILMASTER_QUERY_URL)); switch(request().key) { case Title: u.setPath(u.path() + QLatin1String("film/")); break; case Person: u.setPath(u.path() + QLatin1String("person/")); break; default: break; } QUrlQuery q; q.addQueryItem(QStringLiteral("phrase"), request().value); u.setQuery(q); // myDebug() << "url:" << u; QPointer job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(job, GUI::Proxy::widget()); - connect(job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(job.data(), &KJob::result, this, &FilmasterFetcher::slotComplete); } void FilmasterFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr FilmasterFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } const QString image = entry->field(QStringLiteral("cover")); if(image.contains(QLatin1Char('/'))) { QUrl imageUrl; if(image.startsWith(QLatin1String("//"))) { imageUrl = QUrl(QLatin1String("http:") + image); } else { imageUrl = QUrl(QString::fromLatin1(FILMASTER_API_URL)); imageUrl.setPath(imageUrl.path() + image); } const QString id = ImageFactory::addImage(imageUrl, true); if(id.isEmpty()) { myDebug() << "Failed to load" << imageUrl; message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } // empty image ID is ok entry->setField(QStringLiteral("cover"), id); } return entry; } Tellico::Fetch::FetchRequest FilmasterFetcher::updateRequest(Data::EntryPtr entry_) { const QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } void FilmasterFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); // myDebug(); if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from filmasterfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap resultsMap = doc.object().toVariantMap(); QVariantList resultList; switch(request().key) { case Title: resultList = resultsMap.value(QStringLiteral("best_results")).toList() + resultsMap.value(QStringLiteral("results")).toList(); break; case Person: { const QVariantList personList = resultsMap.value(QStringLiteral("best_results")).toList(); QStringList uris; foreach(const QVariant& person, personList) { const QVariantMap personMap = person.toMap(); uris << mapValue(personMap, "films_played_uri"); uris << mapValue(personMap, "films_directed_uri"); } foreach(const QString& uri, uris) { QUrl u(QString::fromLatin1(FILMASTER_API_URL)); u.setPath(uri); QString output = FileHandler::readTextFile(u, false /*quiet*/, true /*utf8*/); QJsonDocument doc2 = QJsonDocument::fromJson(output.toUtf8()); resultList += doc2.object().toVariantMap().value(QStringLiteral("objects")).toList(); } } break; case Keyword: resultList = resultsMap.value(QStringLiteral("films")).toMap().value(QStringLiteral("best_results")).toList(); break; default: break; } if(resultList.isEmpty()) { myDebug() << "no results"; stop(); return; } Data::CollPtr coll(new Data::VideoCollection(true)); if(!coll->hasField(QStringLiteral("filmaster")) && optionalFields().contains(QStringLiteral("filmaster"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("filmaster"), i18n("Filmaster Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } foreach(const QVariant& result, resultList) { // myDebug() << "found result:" << result; Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, result.toMap()); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } // m_start = m_entries.count(); // m_hasMoreResults = m_start <= m_total; m_hasMoreResults = false; // for now, no continued searches stop(); } void FilmasterFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& result_) { entry_->setField(QStringLiteral("title"), mapValue(result_, "title")); entry_->setField(QStringLiteral("year"), mapValue(result_, "release_year")); entry_->setField(QStringLiteral("genre"), mapValue(result_, "tags")); entry_->setField(QStringLiteral("nationality"), mapValue(result_, "production_country_list")); entry_->setField(QStringLiteral("cover"), mapValue(result_, "image")); entry_->setField(QStringLiteral("plot"), mapValue(result_, "description")); QStringList directors; foreach(const QVariant& director, result_.value(QLatin1String("directors")).toList()) { const QVariantMap directorMap = director.toMap(); directors << mapValue(directorMap, "name") + QLatin1Char(' ') + mapValue(directorMap, "surname"); } if(!directors.isEmpty()) { entry_->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString())); } const QString castUri = mapValue(result_, "characters_uri"); if(!castUri.isEmpty()) { QUrl u(QString::fromLatin1(FILMASTER_API_URL)); u.setPath(castUri); QString output = FileHandler::readTextFile(u, false /*quiet*/, true /*utf8*/); QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); QVariantList castList = doc.object().toVariantMap().value(QStringLiteral("objects")).toList(); QStringList castLines; foreach(const QVariant& castResult, castList) { const QVariantMap castMap = castResult.toMap(); const QVariantMap nameMap = castMap.value(QStringLiteral("person")).toMap(); castLines << mapValue(nameMap, "name") + QLatin1Char(' ') + mapValue(nameMap, "surname") + FieldFormat::columnDelimiterString() + mapValue(castMap, "character"); } if(!castLines.isEmpty()) { entry_->setField(QStringLiteral("cast"), castLines.join(FieldFormat::rowDelimiterString())); } } if(optionalFields().contains(QStringLiteral("filmaster"))) { entry_->setField(QStringLiteral("filmaster"), QLatin1String("http://filmaster.com/film/") + mapValue(result_, "permalink")); } } Tellico::Fetch::ConfigWidget* FilmasterFetcher::configWidget(QWidget* parent_) const { return new FilmasterFetcher::ConfigWidget(parent_, this); } QString FilmasterFetcher::defaultName() { return QStringLiteral("Filmaster"); // no translation } QString FilmasterFetcher::defaultIcon() { return favIcon("http://www.filmaster.com"); } Tellico::StringHash FilmasterFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("filmaster")] = i18n("Filmaster Link"); return hash; } FilmasterFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const FilmasterFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(FilmasterFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void FilmasterFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString FilmasterFetcher::ConfigWidget::preferredName() const { return FilmasterFetcher::defaultName(); } diff --git a/src/fetch/gcstarpluginfetcher.cpp b/src/fetch/gcstarpluginfetcher.cpp index ffc4e61f..181c956d 100644 --- a/src/fetch/gcstarpluginfetcher.cpp +++ b/src/fetch/gcstarpluginfetcher.cpp @@ -1,482 +1,483 @@ /*************************************************************************** Copyright (C) 2005-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 . * * * ***************************************************************************/ #include "gcstarpluginfetcher.h" #include "gcstarthread.h" #include "fetchmanager.h" #include "../collection.h" #include "../entry.h" #include "../translators/gcstarimporter.h" #include "../gui/combobox.h" #include "../gui/collectiontypecombo.h" #include "../utils/cursorsaver.h" #include "../core/filehandler.h" #include "../utils/guiproxy.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::Fetch::GCstarPluginFetcher; GCstarPluginFetcher::CollectionPlugins GCstarPluginFetcher::collectionPlugins; GCstarPluginFetcher::PluginParse GCstarPluginFetcher::pluginParse = NotYet; //static GCstarPluginFetcher::PluginList GCstarPluginFetcher::plugins(int collType_) { if(!collectionPlugins.contains(collType_)) { GUI::CursorSaver cs; QString gcstar = QStandardPaths::findExecutable(QStringLiteral("gcstar")); if(pluginParse == NotYet) { KProcess proc; proc.setProgram(gcstar, QStringList() << QStringLiteral("--version")); proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); // wait 5 seconds at most, just a sanity thing, never want to block completely if(proc.execute(5000) > -1) { QString output = QString::fromLocal8Bit(proc.readAllStandardOutput()); if(!output.isEmpty()) { // always going to be x.y[.z] ? QRegExp versionRx(QLatin1String("(\\d+)\\.(\\d+)(?:\\.(\\d+))?")); if(versionRx.indexIn(output) > -1) { int x = versionRx.cap(1).toInt(); int y = versionRx.cap(2).toInt(); int z = versionRx.cap(3).toInt(); // ok to be empty myDebug() << QStringLiteral("found %1.%2.%3").arg(x).arg(y).arg(z); // --list-plugins argument was added for 1.3 release pluginParse = (x >= 1 && y >=3) ? New : Old; } } } // if still zero, then we should use old in future if(pluginParse == NotYet) { pluginParse = Old; } } if(pluginParse == New) { readPluginsNew(collType_, gcstar); } else { readPluginsOld(collType_, gcstar); } } return collectionPlugins.contains(collType_) ? collectionPlugins.value(collType_) : GCstarPluginFetcher::PluginList(); } void GCstarPluginFetcher::readPluginsNew(int collType_, const QString& gcstar_) { PluginList plugins; const QString gcstarCollection = gcstarType(collType_); if(gcstarCollection.isEmpty()) { collectionPlugins.insert(collType_, plugins); return; } QStringList args; args << QStringLiteral("--execute") << QStringLiteral("--list-plugins") << QStringLiteral("--collection") << gcstarCollection; KProcess proc; proc.setProgram(gcstar_, args); proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); if(proc.execute() < 0) { myWarning() << "can't start"; return; } bool hasName = false; PluginInfo info; QTextStream stream(&proc); for(QString line = stream.readLine(); !stream.atEnd(); line = stream.readLine()) { if(line.isEmpty()) { if(hasName) { plugins << info; } hasName = false; info.clear(); } else { // authors have \t at beginning line = line.trimmed(); if(!hasName) { info.insert(QStringLiteral("name"), line); hasName = true; } else { info.insert(QStringLiteral("author"), line); } // myDebug() << line; } } collectionPlugins.insert(collType_, plugins); } void GCstarPluginFetcher::readPluginsOld(int collType_, const QString& gcstar_) { QDir dir(gcstar_, QStringLiteral("GC*.pm")); dir.cd(QStringLiteral("../../lib/gcstar/GCPlugins/")); QRegExp rx(QLatin1String("get(Name|Author|Lang)\\s*\\{\\s*return\\s+['\"](.+)['\"]")); rx.setMinimal(true); PluginList plugins; const QString dirName = gcstarType(collType_); if(dirName.isEmpty()) { collectionPlugins.insert(collType_, plugins); return; } foreach(const QString& file, dir.entryList()) { QUrl u = QUrl::fromLocalFile(dir.filePath(file)); PluginInfo info; QString text = FileHandler::readTextFile(u); for(int pos = rx.indexIn(text); pos > -1; pos = rx.indexIn(text, pos+rx.matchedLength())) { info.insert(rx.cap(1).toLower(), rx.cap(2)); } // only add if it has a name if(info.contains(QStringLiteral("name"))) { plugins << info; } } // inserting empty list is ok collectionPlugins.insert(collType_, plugins); } QString GCstarPluginFetcher::gcstarType(int collType_) { switch(collType_) { case Data::Collection::Book: return QStringLiteral("GCbooks"); case Data::Collection::Video: return QStringLiteral("GCfilms"); case Data::Collection::Album: return QStringLiteral("GCmusics"); case Data::Collection::ComicBook: return QStringLiteral("GCcomics"); case Data::Collection::Wine: return QStringLiteral("GCwines"); case Data::Collection::Coin: return QStringLiteral("GCcoins"); case Data::Collection::Stamp: return QStringLiteral("GCstamps"); case Data::Collection::Game: return QStringLiteral("GCgames"); case Data::Collection::BoardGame: return QStringLiteral("GCboardgames"); default: break; } return QString(); } GCstarPluginFetcher::GCstarPluginFetcher(QObject* parent_) : Fetcher(parent_), m_started(false), m_collType(-1), m_thread(nullptr) { } GCstarPluginFetcher::~GCstarPluginFetcher() { stop(); } QString GCstarPluginFetcher::source() const { return m_name; } bool GCstarPluginFetcher::canFetch(int type_) const { return m_collType == -1 ? false : m_collType == type_; } void GCstarPluginFetcher::readConfigHook(const KConfigGroup& config_) { m_collType = config_.readEntry("CollectionType", -1); m_plugin = config_.readEntry("Plugin"); } void GCstarPluginFetcher::search() { m_started = true; if(m_plugin.isEmpty() || m_collType == -1) { myWarning() << "no plugin information!"; myDebug() << m_collType << m_plugin; stop(); return; } m_data.clear(); const QString gcstar = QStandardPaths::findExecutable(QStringLiteral("gcstar")); if(gcstar.isEmpty()) { myWarning() << "gcstar not found!"; stop(); return; } QStringList args; args << QStringLiteral("--execute") << QStringLiteral("--collection") << gcstarType(m_collType) << QStringLiteral("--export") << QStringLiteral("TarGz") << QStringLiteral("--exportprefs") << QStringLiteral("collection=>/tmp/test.gcs,file=>/tmp/test1.tar.gz") << QStringLiteral("--website") << m_plugin << QStringLiteral("--download") << KShell::quoteArg(request().value); myLog() << args; m_thread = new GCstarThread(this); m_thread->setProgram(gcstar, args); - connect(m_thread, SIGNAL(standardOutput(QByteArray)), SLOT(slotData(QByteArray))); - connect(m_thread, SIGNAL(standardError(QByteArray)), SLOT(slotError(QByteArray))); - connect(m_thread, SIGNAL(finished()), SLOT(slotProcessExited())); + connect(m_thread, &GCstarThread::standardOutput, this, &GCstarPluginFetcher::slotData); + connect(m_thread, &GCstarThread::standardError, this, &GCstarPluginFetcher::slotError); + connect(m_thread, &QThread::finished, this, &GCstarPluginFetcher::slotProcessExited); m_thread->start(); } void GCstarPluginFetcher::stop() { if(!m_started) { return; } if(m_thread) { if(m_thread->isRunning()) { m_thread->terminate(); m_thread->wait(); } delete m_thread; m_thread = nullptr; } m_data.clear(); m_started = false; m_errors.clear(); emit signalDone(this); } void GCstarPluginFetcher::slotData(const QByteArray& data_) { m_data.append(data_); } void GCstarPluginFetcher::slotError(const QByteArray& data_) { QString msg = QString::fromLocal8Bit(data_); msg.prepend(source() + QLatin1String(": ")); myDebug() << msg; m_errors << msg; } void GCstarPluginFetcher::slotProcessExited() { // if stop() is called and the thread terminated // the finished() signal will still fire if(!m_started) { return; } if(!m_errors.isEmpty()) { message(m_errors.join(QLatin1String("\n")), MessageHandler::Warning); } if(m_data.isEmpty()) { myDebug() << source() << ": no data"; stop(); return; } QBuffer filterBuffer(&m_data); KCompressionDevice::CompressionType compressionType = KFilterDev::compressionTypeForMimeType(QStringLiteral("application/x-gzip")); KCompressionDevice filter(&filterBuffer, false, compressionType); if(!filter.open(QIODevice::ReadOnly)) { myWarning() << "unable to open gzip filter"; stop(); return; } QByteArray tarData = filter.readAll(); QBuffer buffer(&tarData); KTar tar(&buffer); if(!tar.open(QIODevice::ReadOnly)) { myWarning() << "unable to open tar file"; stop(); return; } const KArchiveDirectory* dir = tar.directory(); if(!dir) { myWarning() << "unable to open tar directory"; stop(); return; } QTemporaryDir tempDir; dir->copyTo(tempDir.path()); // KDE seems to have a bug (#252821) for gcstar files where the images are not in the images/ directory foreach(const QString& filename, dir->entries()) { if(dir->entry(filename)->isFile() && filename != QLatin1String("collection.gcs")) { const KArchiveFile* f = static_cast(dir->entry(filename)); f->copyTo(tempDir.path() + QLatin1String("/images")); } } QUrl gcsUrl = QUrl::fromLocalFile(tempDir.path()); gcsUrl = gcsUrl.adjusted(QUrl::StripTrailingSlash); gcsUrl.setPath(gcsUrl.path() + QLatin1String("/collection.gcs")); Import::GCstarImporter imp(gcsUrl); imp.setHasRelativeImageLinks(true); Data::CollPtr coll = imp.collection(); if(!coll) { if(!imp.statusMessage().isEmpty()) { message(imp.statusMessage(), MessageHandler::Status); } myWarning() << "no collection pointer"; stop(); return; } foreach(Data::EntryPtr entry, coll->entries()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); if(!m_started) { return; } } stop(); // be sure to call this } Tellico::Data::EntryPtr GCstarPluginFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest GCstarPluginFetcher::updateRequest(Data::EntryPtr entry_) { // ry searching for title and rely on Collection::sameEntry() to figure things out QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* GCstarPluginFetcher::configWidget(QWidget* parent_) const { return new GCstarPluginFetcher::ConfigWidget(parent_, this); } QString GCstarPluginFetcher::defaultName() { return i18n("GCstar Plugin"); } QString GCstarPluginFetcher::defaultIcon() { return QStringLiteral("gcstar"); } GCstarPluginFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GCstarPluginFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_), m_needPluginList(true) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); l->addWidget(label, ++row, 0); m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); - connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); - connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotTypeChanged())); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_collCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_collCombo, activatedInt, this, &ConfigWidget::slotTypeChanged); l->addWidget(m_collCombo, row, 1, 1, 3); QString w = i18n("Set the collection type of the data returned from the plugin."); label->setWhatsThis(w); m_collCombo->setWhatsThis(w); label->setBuddy(m_collCombo); label = new QLabel(i18n("&Plugin: "), optionsWidget()); l->addWidget(label, ++row, 0); m_pluginCombo = new GUI::ComboBox(optionsWidget()); - connect(m_pluginCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); - connect(m_pluginCombo, SIGNAL(activated(int)), SLOT(slotPluginChanged())); + connect(m_pluginCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_pluginCombo, activatedInt, this, &ConfigWidget::slotPluginChanged); l->addWidget(m_pluginCombo, row, 1, 1, 3); w = i18n("Select the GCstar plugin used for the data source."); label->setWhatsThis(w); m_pluginCombo->setWhatsThis(w); label->setBuddy(m_pluginCombo); label = new QLabel(i18n("Author: "), optionsWidget()); l->addWidget(label, ++row, 0); m_authorLabel = new QLabel(optionsWidget()); l->addWidget(m_authorLabel, row, 1); if(fetcher_) { if(fetcher_->m_collType > -1) { m_collCombo->setCurrentType(fetcher_->m_collType); } else { m_collCombo->setCurrentType(fetcher_->collectionType()); } m_originalPluginName = fetcher_->m_plugin; } else { // default to Book for now m_collCombo->setCurrentType(Data::Collection::Book); } KAcceleratorManager::manage(optionsWidget()); } GCstarPluginFetcher::ConfigWidget::~ConfigWidget() { } void GCstarPluginFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { config_.writeEntry("CollectionType", m_collCombo->currentType()); config_.writeEntry("Plugin", m_pluginCombo->currentText()); } QString GCstarPluginFetcher::ConfigWidget::preferredName() const { QString plugin = m_pluginCombo->currentText(); return plugin.isEmpty() ? plugin : QLatin1String("GCstar - ") + plugin; } void GCstarPluginFetcher::ConfigWidget::slotTypeChanged() { int collType = m_collCombo->currentType(); m_pluginCombo->clear(); QStringList pluginNames; GCstarPluginFetcher::PluginList list = GCstarPluginFetcher::plugins(collType); foreach(const GCstarPluginFetcher::PluginInfo& info, list) { pluginNames << info.value(QStringLiteral("name")).toString(); m_pluginCombo->addItem(pluginNames.last(), info); } slotPluginChanged(); emit signalName(preferredName()); } void GCstarPluginFetcher::ConfigWidget::slotPluginChanged() { PluginInfo info = m_pluginCombo->currentData().toHash(); m_authorLabel->setText(info[QStringLiteral("author")].toString()); emit signalName(preferredName()); } void GCstarPluginFetcher::ConfigWidget::showEvent(QShowEvent*) { if(m_needPluginList) { m_needPluginList = false; slotTypeChanged(); // update plugin combo box if(!m_originalPluginName.isEmpty()) { m_pluginCombo->setEditText(m_originalPluginName); slotPluginChanged(); } } } diff --git a/src/fetch/gcstarthread.cpp b/src/fetch/gcstarthread.cpp index 175a3f7f..608b52b8 100644 --- a/src/fetch/gcstarthread.cpp +++ b/src/fetch/gcstarthread.cpp @@ -1,54 +1,54 @@ /*************************************************************************** Copyright (C) 2010 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 "gcstarthread.h" #include using Tellico::Fetch::GCstarThread; GCstarThread::GCstarThread(QObject* obj) : QThread(obj) { } void GCstarThread::setProgram(const QString& program_, const QStringList& args_) { m_program = program_; m_args = args_; } void GCstarThread::run() { KProcess proc; - connect(&proc, SIGNAL(readyReadStandardOutput()), SLOT(slotData())); - connect(&proc, SIGNAL(readyReadStandardError()), SLOT(slotError())); + connect(&proc, &QProcess::readyReadStandardOutput, this, &GCstarThread::slotData); + connect(&proc, &QProcess::readyReadStandardError, this, &GCstarThread::slotError); proc.setOutputChannelMode(KProcess::SeparateChannels); proc.setProgram(m_program, m_args); proc.execute(); } void GCstarThread::slotData() { emit standardOutput(static_cast(sender())->readAllStandardOutput()); } void GCstarThread::slotError() { emit standardError(static_cast(sender())->readAllStandardError()); } diff --git a/src/fetch/giantbombfetcher.cpp b/src/fetch/giantbombfetcher.cpp index f298c463..9fd379de 100644 --- a/src/fetch/giantbombfetcher.cpp +++ b/src/fetch/giantbombfetcher.cpp @@ -1,261 +1,261 @@ /*************************************************************************** Copyright (C) 2010 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 "giantbombfetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include namespace { static const int GIANTBOMB_MAX_RETURNS_TOTAL = 20; static const char* GIANTBOMB_API_URL = "http://api.giantbomb.com"; static const char* GIANTBOMB_API_KEY = "291bfe4b2d77a460e67dd8f90c1e7e56c3e4f05a"; } using namespace Tellico; using Tellico::Fetch::GiantBombFetcher; GiantBombFetcher::GiantBombFetcher(QObject* parent_) : XMLFetcher(parent_) , m_total(-1) , m_apiKey(QLatin1String(GIANTBOMB_API_KEY)) { setLimit(GIANTBOMB_MAX_RETURNS_TOTAL); setXSLTFilename(QStringLiteral("giantbomb2tellico.xsl")); } GiantBombFetcher::~GiantBombFetcher() { } QString GiantBombFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool GiantBombFetcher::canFetch(int type) const { return type == Data::Collection::Game; } void GiantBombFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key", GIANTBOMB_API_KEY); if(!k.isEmpty()) { m_apiKey = k; } } void GiantBombFetcher::resetSearch() { m_total = -1; } QUrl GiantBombFetcher::searchUrl() { QUrl u(QString::fromLatin1(GIANTBOMB_API_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); switch(request().key) { case Keyword: u.setPath(QStringLiteral("/search")); q.addQueryItem(QStringLiteral("query"), request().value); q.addQueryItem(QStringLiteral("resources"), QStringLiteral("game")); break; default: myWarning() << "key not recognized: " << request().key; return QUrl(); } u.setQuery(q); // myDebug() << "url: " << u.url(); return u; } void GiantBombFetcher::parseData(QByteArray& data_) { Q_UNUSED(data_); #if 0 if(m_total == -1) { QDomDocument dom; if(!dom.setContent(data, false)) { myWarning() << "server did not return valid XML."; return; } // total is /resp/fetchresults/@numResults QDomNode n = dom.documentElement().namedItem(QLatin1String("resp")) .namedItem(QLatin1String("fetchresults")); QDomElement e = n.toElement(); if(!e.isNull()) { m_total = e.attribute(QLatin1String("numResults")).toInt(); myDebug() << "total = " << m_total; } } m_start = m_entries.count() + 1; // not sure how to specify start in the REST url // m_hasMoreResults = m_start <= m_total; #endif } Tellico::Data::EntryPtr GiantBombFetcher::fetchEntryHookData(Data::EntryPtr entry_) { Q_ASSERT(entry_); const QString id = entry_->field(QStringLiteral("giantbomb-id")); if(id.isEmpty()) { myDebug() << "no giantbomb id found"; return entry_; } QUrl u(QString::fromLatin1(GIANTBOMB_API_URL)); u.setPath(QStringLiteral("/game/%1/").arg(id)); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); u.setQuery(q); // myDebug() << "url: " << u; // quiet QString output = FileHandler::readXMLFile(u, true); #if 0 myWarning() << "Remove output debug from giantbombfetcher.cpp"; QFile f(QStringLiteral("/tmp/test2.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << output; } f.close(); #endif Import::TellicoImporter imp(xsltHandler()->applyStylesheet(output)); // be quiet when loading images imp.setOptions(imp.options() ^ Import::ImportShowImageErrors); Data::CollPtr coll = imp.collection(); // getTracks(entry); if(!coll) { myWarning() << "no collection pointer"; return entry_; } if(coll->entryCount() > 1) { myDebug() << "weird, more than one entry found"; } // don't want to include id coll->removeField(QStringLiteral("giantbomb-id")); return coll->entries().front(); } Tellico::Fetch::FetchRequest GiantBombFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Keyword, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* GiantBombFetcher::configWidget(QWidget* parent_) const { return new GiantBombFetcher::ConfigWidget(parent_, this); } QString GiantBombFetcher::defaultName() { return QStringLiteral("Giant Bomb"); } QString GiantBombFetcher::defaultIcon() { return favIcon("http://www.giantbomb.com"); } Tellico::StringHash GiantBombFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("giantbomb")] = i18n("GiantBomb Link"); hash[QStringLiteral("pegi")] = i18n("PEGI Rating"); return hash; } GiantBombFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GiantBombFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("http://api.giantbomb.com")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_apiKeyEdit->setWhatsThis(w); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); // now add additional fields widget addFieldsWidget(GiantBombFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); if(fetcher_) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own if(fetcher_->m_apiKey != QLatin1String(GIANTBOMB_API_KEY)) { m_apiKeyEdit->setText(fetcher_->m_apiKey); } } } void GiantBombFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString GiantBombFetcher::ConfigWidget::preferredName() const { return GiantBombFetcher::defaultName(); } diff --git a/src/fetch/googlebookfetcher.cpp b/src/fetch/googlebookfetcher.cpp index f350c720..12eb563f 100644 --- a/src/fetch/googlebookfetcher.cpp +++ b/src/fetch/googlebookfetcher.cpp @@ -1,423 +1,423 @@ /*************************************************************************** Copyright (C) 2011 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 "googlebookfetcher.h" #include "../collections/bookcollection.h" #include "../entry.h" #include "../images/imagefactory.h" #include "../utils/isbnvalidator.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int GOOGLEBOOK_MAX_RETURNS = 20; static const char* GOOGLEBOOK_API_URL = "https://www.googleapis.com/books/v1/volumes"; static const char* GOOGLEBOOK_API_KEY = "AIzaSyBdsa_DEGpDQ6PzZyYHHHokRIBY8thOdUQ"; } using namespace Tellico; using Tellico::Fetch::GoogleBookFetcher; GoogleBookFetcher::GoogleBookFetcher(QObject* parent_) : Fetcher(parent_) , m_started(false) , m_start(0) , m_total(0) , m_apiKey(QLatin1String(GOOGLEBOOK_API_KEY)) { } GoogleBookFetcher::~GoogleBookFetcher() { } QString GoogleBookFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool GoogleBookFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN || k == Keyword; } bool GoogleBookFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } void GoogleBookFetcher::readConfigHook(const KConfigGroup& config_) { // allow an empty key if the config key does exist m_apiKey = config_.readEntry("API Key", GOOGLEBOOK_API_KEY); } void GoogleBookFetcher::search() { m_start = 0; m_total = -1; continueSearch(); } void GoogleBookFetcher::continueSearch() { m_started = true; // we only split ISBN and LCCN values QStringList searchTerms; if(request().key == ISBN) { searchTerms = FieldFormat::splitValue(request().value); } else { searchTerms += request().value; } foreach(const QString& searchTerm, searchTerms) { doSearch(searchTerm); } if(m_jobs.isEmpty()) { stop(); } } void GoogleBookFetcher::doSearch(const QString& term_) { QUrl u(QString::fromLatin1(GOOGLEBOOK_API_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("maxResults"), QString::number(GOOGLEBOOK_MAX_RETURNS)); q.addQueryItem(QStringLiteral("startIndex"), QString::number(m_start)); q.addQueryItem(QStringLiteral("printType"), QStringLiteral("books")); // we don't require a key, cause it might work without it if(!m_apiKey.isEmpty()) { q.addQueryItem(QStringLiteral("key"), m_apiKey); } switch(request().key) { case Title: q.addQueryItem(QStringLiteral("q"), QLatin1String("intitle:") + term_); break; case Person: q.addQueryItem(QStringLiteral("q"), QLatin1String("inauthor:") + term_); break; case ISBN: { const QString isbn = ISBNValidator::cleanValue(term_); q.addQueryItem(QStringLiteral("q"), QLatin1String("isbn:") + isbn); } break; case Keyword: q.addQueryItem(QStringLiteral("q"), term_); break; default: myWarning() << "key not recognized:" << request().key; return; } u.setQuery(q); // myDebug() << "url:" << u; QPointer job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(job, GUI::Proxy::widget()); - connect(job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(job.data(), &KJob::result, this, &GoogleBookFetcher::slotComplete); m_jobs << job; } void GoogleBookFetcher::endJob(KIO::StoredTransferJob* job_) { m_jobs.removeOne(job_); if(m_jobs.isEmpty()) { stop(); } } void GoogleBookFetcher::stop() { if(!m_started) { return; } foreach(QPointer job, m_jobs) { if(job) { job->kill(); } } m_jobs.clear(); m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr GoogleBookFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } QString gbs = entry->field(QStringLiteral("gbs-link")); if(!gbs.isEmpty()) { // quiet QByteArray data = FileHandler::readDataFile(QUrl::fromUserInput(gbs), true); QJsonDocument doc = QJsonDocument::fromJson(data); populateEntry(entry, doc.object().toVariantMap()); } const QString image_id = entry->field(QStringLiteral("cover")); // if it's still a url, we need to load it if(image_id.startsWith(QLatin1String("http"))) { const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); entry->setField(QStringLiteral("cover"), QString()); } else { entry->setField(QStringLiteral("cover"), id); } } // don't want to include gbs json link entry->setField(QStringLiteral("gbs-link"), QString()); return entry; } Tellico::Fetch::FetchRequest GoogleBookFetcher::updateRequest(Data::EntryPtr entry_) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(ISBN, isbn); } QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } void GoogleBookFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); // myDebug(); if(job->error()) { job->uiDelegate()->showErrorMessage(); endJob(job); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; endJob(job); return; } #if 0 myWarning() << "Remove debug from googlebookfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif Data::CollPtr coll(new Data::BookCollection(true)); // always add the gbs-link for fetchEntryHook Data::FieldPtr field(new Data::Field(QStringLiteral("gbs-link"), QStringLiteral("GBS Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); if(!coll->hasField(QStringLiteral("googlebook")) && optionalFields().contains(QStringLiteral("googlebook"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("googlebook"), i18n("Google Book Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap result = doc.object().toVariantMap(); m_total = result.value(QStringLiteral("totalItems")).toInt(); // myDebug() << "total:" << m_total; QVariantList resultList = result.value(QStringLiteral("items")).toList(); if(resultList.isEmpty()) { myDebug() << "no results"; endJob(job); return; } foreach(const QVariant& result, resultList) { // myDebug() << "found result:" << result; Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, result.toMap()); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } m_start = m_entries.count(); m_hasMoreResults = request().key != ISBN && m_start <= m_total; endJob(job); } void GoogleBookFetcher::populateEntry(Data::EntryPtr entry, const QVariantMap& resultMap) { if(entry->collection()->hasField(QStringLiteral("gbs-link"))) { entry->setField(QStringLiteral("gbs-link"), mapValue(resultMap, "selfLink")); } const QVariantMap volumeMap = resultMap.value(QStringLiteral("volumeInfo")).toMap(); entry->setField(QStringLiteral("title"), mapValue(volumeMap, "title")); entry->setField(QStringLiteral("subtitle"), mapValue(volumeMap, "subtitle")); entry->setField(QStringLiteral("pub_year"), mapValue(volumeMap, "publishedDate").left(4)); entry->setField(QStringLiteral("author"), mapValue(volumeMap, "authors")); // workaround for bug, where publisher can be enclosed in quotes QString pub = mapValue(volumeMap, "publisher"); if(pub.startsWith(QLatin1Char('"')) && pub.endsWith(QLatin1Char('"'))) { pub.chop(1); pub = pub.remove(0, 1); } entry->setField(QStringLiteral("publisher"), pub); entry->setField(QStringLiteral("pages"), mapValue(volumeMap, "pageCount")); entry->setField(QStringLiteral("language"), mapValue(volumeMap, "language")); entry->setField(QStringLiteral("comments"), mapValue(volumeMap, "description")); QStringList catList = volumeMap.value(QStringLiteral("categories")).toStringList(); // google is going to give us a lot of categories QSet cats; foreach(const QString& cat, catList) { cats += cat.split(QRegExp(QLatin1String("\\s*/\\s*"))).toSet(); } // remove General cats.remove(QStringLiteral("General")); catList = cats.toList(); catList.sort(); entry->setField(QStringLiteral("keyword"), catList.join(FieldFormat::delimiterString())); QString isbn; foreach(const QVariant& idVariant, volumeMap.value(QLatin1String("industryIdentifiers")).toList()) { const QVariantMap idMap = idVariant.toMap(); if(mapValue(idMap, "type") == QLatin1String("ISBN_10")) { isbn = mapValue(idMap, "identifier"); break; } else if(mapValue(idMap, "type") == QLatin1String("ISBN_13")) { isbn = mapValue(idMap, "identifier"); // allow isbn10 to override, so don't break here } } if(!isbn.isEmpty()) { ISBNValidator val(this); val.fixup(isbn); entry->setField(QStringLiteral("isbn"), isbn); } const QVariantMap imageMap = volumeMap.value(QStringLiteral("imageLinks")).toMap(); if(imageMap.contains(QStringLiteral("small"))) { entry->setField(QStringLiteral("cover"), mapValue(imageMap, "small")); } else if(imageMap.contains(QStringLiteral("thumbnail"))) { entry->setField(QStringLiteral("cover"), mapValue(imageMap, "thumbnail")); } else if(imageMap.contains(QStringLiteral("smallThumbnail"))) { entry->setField(QStringLiteral("cover"), mapValue(imageMap, "smallThumbnail")); } if(optionalFields().contains(QStringLiteral("googlebook"))) { entry->setField(QStringLiteral("googlebook"), mapValue(volumeMap, "infoLink")); } } Tellico::Fetch::ConfigWidget* GoogleBookFetcher::configWidget(QWidget* parent_) const { return new GoogleBookFetcher::ConfigWidget(parent_, this); } QString GoogleBookFetcher::defaultName() { return i18n("Google Book Search"); } QString GoogleBookFetcher::defaultIcon() { return favIcon("http://books.google.com"); } Tellico::StringHash GoogleBookFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("googlebook")] = i18n("Google Book Link"); return hash; } GoogleBookFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GoogleBookFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("https://code.google.com/apis/console")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_apiKeyEdit->setWhatsThis(w); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); // now add additional fields widget addFieldsWidget(GoogleBookFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); if(fetcher_ && fetcher_->m_apiKey != QLatin1String(GOOGLEBOOK_API_KEY)) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own m_apiKeyEdit->setText(fetcher_->m_apiKey); } } void GoogleBookFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString GoogleBookFetcher::ConfigWidget::preferredName() const { return GoogleBookFetcher::defaultName(); } diff --git a/src/fetch/googlescholarfetcher.cpp b/src/fetch/googlescholarfetcher.cpp index 55593ed3..b2a05ae0 100644 --- a/src/fetch/googlescholarfetcher.cpp +++ b/src/fetch/googlescholarfetcher.cpp @@ -1,286 +1,286 @@ /*************************************************************************** Copyright (C) 2008-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 . * * * ***************************************************************************/ #include "googlescholarfetcher.h" #include "../core/filehandler.h" #include "../translators/bibteximporter.h" #include "../collections/bibtexcollection.h" #include "../entry.h" #include "../utils/guiproxy.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include namespace { static const int GOOGLE_MAX_RETURNS_TOTAL = 20; static const char* SCHOLAR_BASE_URL = "http://scholar.google.com/scholar"; static const char* SCHOLAR_SET_CONFIG_URL = "http://scholar.google.com/scholar_settings?hl=en&as_sdt=0,5"; static const char* SCHOLAR_SET_BIBTEX_URL = "http://scholar.google.com/scholar_setprefs?hl=en&num=100&scis=yes&scisf=4&submit="; } using namespace Tellico; using Tellico::Fetch::GoogleScholarFetcher; GoogleScholarFetcher::GoogleScholarFetcher(QObject* parent_) : Fetcher(parent_), m_limit(GOOGLE_MAX_RETURNS_TOTAL), m_start(0), m_total(0), m_job(nullptr), m_started(false), m_cookieIsSet(false) { m_bibtexRx = QRegExp(QLatin1String("]*scholar\\.bib[^>]*)\"")); m_bibtexRx.setMinimal(true); } GoogleScholarFetcher::~GoogleScholarFetcher() { } QString GoogleScholarFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool GoogleScholarFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void GoogleScholarFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void GoogleScholarFetcher::search() { if(!m_cookieIsSet) { setBibtexCookie(); } m_started = true; m_start = 0; m_total = -1; doSearch(); } void GoogleScholarFetcher::continueSearch() { m_started = true; doSearch(); } void GoogleScholarFetcher::doSearch() { // myDebug() << "value = " << value_; QUrl u(QString::fromLatin1(SCHOLAR_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("start"), QString::number(m_start)); QString value = request().value; if(!value.startsWith(QLatin1Char('"'))) { value = QLatin1Char('"') + value; } if(!value.endsWith(QLatin1Char('"'))) { value += QLatin1Char('"'); } switch(request().key) { case Title: q.addQueryItem(QStringLiteral("q"), QStringLiteral("allintitle:%1").arg(request().value)); break; case Keyword: q.addQueryItem(QStringLiteral("q"), request().value); break; case Person: q.addQueryItem(QStringLiteral("q"), QStringLiteral("author:%1").arg(request().value)); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &GoogleScholarFetcher::slotComplete); } void GoogleScholarFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void GoogleScholarFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; const QString text = QString::fromUtf8(data.constData(), data.size()); #if 0 myWarning() << "Remove debug from googlescholarfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << text; } f.close(); #endif QString bibtex; int count = 0; for(int pos = m_bibtexRx.indexIn(text); count < m_limit && pos > -1; pos = m_bibtexRx.indexIn(text, pos+m_bibtexRx.matchedLength()), ++count) { // for some reason, KIO and google don't return bibtex when '&' is escaped QString url = m_bibtexRx.cap(1).replace(QLatin1String("&"), QLatin1String("&")); QUrl bibtexUrl = QUrl(QString::fromLatin1(SCHOLAR_BASE_URL)).resolved(QUrl(url)); // myDebug() << bibtexUrl; bibtex += FileHandler::readTextFile(bibtexUrl, true); } Import::BibtexImporter imp(bibtex); // quiet warnings... imp.setCurrentCollection(Data::CollPtr(new Data::BibtexCollection(true))); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no collection pointer"; stop(); return; } count = 0; Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { if(count >= m_limit) { break; } if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, Data::EntryPtr(entry)); emit signalResultFound(r); ++count; } m_start = m_entries.count(); // m_hasMoreResults = m_start <= m_total; m_hasMoreResults = false; // for now, no continued searches stop(); // required } Tellico::Data::EntryPtr GoogleScholarFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest GoogleScholarFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* GoogleScholarFetcher::configWidget(QWidget* parent_) const { return new GoogleScholarFetcher::ConfigWidget(parent_, this); } QString GoogleScholarFetcher::defaultName() { // no i18n return QStringLiteral("Google Scholar"); } QString GoogleScholarFetcher::defaultIcon() { return favIcon("http://scholar.google.com"); } void GoogleScholarFetcher::setBibtexCookie() { // it appears that the series of url reads are necessary to get the correct cookie set // have to set preferences to have bibtex output const QString text = FileHandler::readTextFile(QUrl(QString::fromLatin1(SCHOLAR_SET_CONFIG_URL)), true); // find hidden input variables QRegExp inputRx(QLatin1String("]*\\s*type\\s*=\\s*\"hidden\"\\s+[^>]+>")); inputRx.setMinimal(true); QRegExp pairRx(QLatin1String("([^=\\s<]+)\\s*=\\s*\"?([^=\\s\">]+)\"?")); QHash nameValues; for(int pos = inputRx.indexIn(text); pos > -1; pos = inputRx.indexIn(text, pos+inputRx.matchedLength())) { const QString input = inputRx.cap(0); QString name, value; for(int pos2 = pairRx.indexIn(input); pos2 > -1; pos2 = pairRx.indexIn(input, pos2+pairRx.matchedLength())) { if(pairRx.cap(1).toLower() == QLatin1String("name")) { name = pairRx.cap(2); } else if(pairRx.cap(1).toLower() == QLatin1String("value")) { value = pairRx.cap(2); } } if(!name.isEmpty() && !value.isEmpty()) { nameValues.insert(name, value); } } QString newUrl = QLatin1String(SCHOLAR_SET_BIBTEX_URL); for(QHash::const_iterator i = nameValues.constBegin(); i != nameValues.constEnd(); ++i) { newUrl += QLatin1Char('&') + i.key() + QLatin1Char('=') + i.value(); } FileHandler::readTextFile(QUrl(newUrl), true); m_cookieIsSet = true; } GoogleScholarFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GoogleScholarFetcher* /*=0*/) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); } QString GoogleScholarFetcher::ConfigWidget::preferredName() const { return GoogleScholarFetcher::defaultName(); } diff --git a/src/fetch/hathitrustfetcher.cpp b/src/fetch/hathitrustfetcher.cpp index 38367e6f..4b180e3c 100644 --- a/src/fetch/hathitrustfetcher.cpp +++ b/src/fetch/hathitrustfetcher.cpp @@ -1,325 +1,325 @@ /*************************************************************************** Copyright (C) 2012 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 "hathitrustfetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/isbnvalidator.h" #include "../utils/lccnvalidator.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../utils/datafileregistry.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* HATHITRUST_QUERY_URL = "http://catalog.hathitrust.org/api/volumes/full/json/"; } using namespace Tellico; using Tellico::Fetch::HathiTrustFetcher; HathiTrustFetcher::HathiTrustFetcher(QObject* parent_) : Fetcher(parent_), m_started(false), m_MARC21XMLHandler(nullptr), m_MODSHandler(nullptr) { } HathiTrustFetcher::~HathiTrustFetcher() { delete m_MARC21XMLHandler; m_MARC21XMLHandler = nullptr; delete m_MODSHandler; m_MODSHandler = nullptr; } QString HathiTrustFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool HathiTrustFetcher::canSearch(FetchKey k) const { return k == ISBN || k == LCCN; } bool HathiTrustFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } void HathiTrustFetcher::readConfigHook(const KConfigGroup&) { } void HathiTrustFetcher::search() { m_started = true; doSearch(); } void HathiTrustFetcher::doSearch() { QUrl u(QString::fromLatin1(HATHITRUST_QUERY_URL)); QStringList searchValues; // we split ISBN and LCCN values, which are the only ones we accept anyway const QStringList searchTerms = FieldFormat::splitValue(request().value); foreach(const QString& searchTerm, searchTerms) { if(request().key == ISBN) { searchValues += QStringLiteral("isbn:%1").arg(ISBNValidator::cleanValue(searchTerm)); } else { searchValues += QStringLiteral("lccn:%1").arg(LCCNValidator::formalize(searchTerm)); } } u.setPath(u.path() + searchValues.join(QLatin1String("|"))); // myDebug() << u; m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &HathiTrustFetcher::slotComplete); } void HathiTrustFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); } m_started = false; emit signalDone(this); } bool HathiTrustFetcher::initMARC21Handler() { if(m_MARC21XMLHandler) { return true; } QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("MARC21slim2MODS3.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate MARC21slim2MODS3.xsl."; return false; } QUrl u = QUrl::fromLocalFile(xsltfile); m_MARC21XMLHandler = new XSLTHandler(u); if(!m_MARC21XMLHandler->isValid()) { myWarning() << "error in MARC21slim2MODS3.xsl."; delete m_MARC21XMLHandler; m_MARC21XMLHandler = nullptr; return false; } return true; } bool HathiTrustFetcher::initMODSHandler() { if(m_MODSHandler) { return true; } QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("mods2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate mods2tellico.xsl."; return false; } QUrl u = QUrl::fromLocalFile(xsltfile); m_MODSHandler = new XSLTHandler(u); if(!m_MODSHandler->isValid()) { myWarning() << "error in mods2tellico.xsl."; delete m_MODSHandler; m_MODSHandler = nullptr; // no use in keeping the MARC handlers now delete m_MARC21XMLHandler; m_MARC21XMLHandler = nullptr; return false; } return true; } Tellico::Data::EntryPtr HathiTrustFetcher::fetchEntryHook(uint uid_) { return m_entries.value(uid_); } Tellico::Fetch::FetchRequest HathiTrustFetcher::updateRequest(Data::EntryPtr entry_) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(ISBN, isbn); } const QString lccn = entry_->field(QStringLiteral("lccn")); if(!lccn.isEmpty()) { return FetchRequest(LCCN, lccn); } return FetchRequest(); } void HathiTrustFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); if(!initMARC21Handler() || !initMODSHandler()) { // debug messages are taken care of in the specific methods stop(); return; } if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from hathitrustfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap resultMap = doc.object().toVariantMap(); if(resultMap.isEmpty()) { myDebug() << "no results"; stop(); return; } QVariantMap::const_iterator i = resultMap.constBegin(); for( ; i != resultMap.constEnd(); ++i) { const QVariantMap recordMap = i.value().toMap().value(QStringLiteral("records")).toMap(); if(recordMap.isEmpty()) { myDebug() << "empty result map"; continue; } // we know there's a record, so no need to check for existence of first iterator in map QVariantMap::const_iterator ri = recordMap.constBegin(); if(ri == recordMap.constEnd()) { myWarning() << "no iterator in record"; continue; } QString marcxml = ri.value().toMap().value(QStringLiteral("marc-xml")).toString(); // HathiTrust doesn't always include the XML NS in the JSON results. Assume it's always // MARC XML and check that QDomDocument dom; if(dom.setContent(marcxml, true /* namespace processing */) && dom.documentElement().namespaceURI().isEmpty()) { const QString rootName = dom.documentElement().tagName(); myDebug() << "no namespace, attempting to set on" << rootName << "element"; QRegExp rootRx(QLatin1Char('<') + rootName + QLatin1Char('>')); QString newRoot = QLatin1Char('<') + rootName + QLatin1String(" xmlns=\"http://www.loc.gov/MARC21/slim\">"); marcxml.replace(rootRx, newRoot); } const QString modsxml = m_MARC21XMLHandler->applyStylesheet(marcxml); Import::TellicoImporter imp(m_MODSHandler->applyStylesheet(modsxml)); imp.setOptions(imp.options() ^ Import::ImportProgress); // no progress needed Data::CollPtr coll = imp.collection(); if(!coll) { myWarning() << "no coll pointer"; continue; } // since the Dewey and LoC field titles have a context in their i18n call here // but not in the mods2tellico.xsl stylesheet where the field is actually created // update the field titles here QHashIterator i(allOptionalFields()); while(i.hasNext()) { i.next(); Data::FieldPtr field = coll->fieldByName(i.key()); if(field) { field->setTitle(i.value()); coll->modifyField(field); } } foreach(Data::EntryPtr entry, coll->entries()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } } m_hasMoreResults = false; // for now, no continued searches stop(); } Tellico::Fetch::ConfigWidget* HathiTrustFetcher::configWidget(QWidget* parent_) const { return new HathiTrustFetcher::ConfigWidget(parent_, this); } QString HathiTrustFetcher::defaultName() { return QStringLiteral("HathiTrust"); // no translation } QString HathiTrustFetcher::defaultIcon() { return favIcon("http://www.hathitrust.org"); } Tellico::StringHash HathiTrustFetcher::allOptionalFields() { // same ones as z3950fetcher StringHash hash; hash[QStringLiteral("address")] = i18n("Address"); hash[QStringLiteral("abstract")] = i18n("Abstract"); hash[QStringLiteral("illustrator")] = i18n("Illustrator"); hash[QStringLiteral("dewey")] = i18nc("Dewey Decimal classification system", "Dewey Decimal"); hash[QStringLiteral("lcc")] = i18nc("Library of Congress classification system", "LoC Classification"); return hash; } HathiTrustFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const HathiTrustFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(HathiTrustFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void HathiTrustFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString HathiTrustFetcher::ConfigWidget::preferredName() const { return HathiTrustFetcher::defaultName(); } diff --git a/src/fetch/ibsfetcher.cpp b/src/fetch/ibsfetcher.cpp index 37b7c254..332b3f76 100644 --- a/src/fetch/ibsfetcher.cpp +++ b/src/fetch/ibsfetcher.cpp @@ -1,366 +1,366 @@ /*************************************************************************** Copyright (C) 2006-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 . * * * ***************************************************************************/ #include "ibsfetcher.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collections/bookcollection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../images/imagefactory.h" #include "../utils/isbnvalidator.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* IBS_BASE_URL = "https://www.ibs.it/search/"; } using namespace Tellico; using Tellico::Fetch::IBSFetcher; IBSFetcher::IBSFetcher(QObject* parent_) : Fetcher(parent_), m_total(0), m_started(false) { } IBSFetcher::~IBSFetcher() { } QString IBSFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool IBSFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } // No UPC or Raw for now. bool IBSFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN; } void IBSFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void IBSFetcher::search() { m_started = true; m_matches.clear(); QUrl u(QString::fromLatin1(IBS_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("ts"), QStringLiteral("as")); switch(request().key) { case Title: { // can't have ampersands QString s = request().value; s.remove(QLatin1Char('&')); q.addQueryItem(QStringLiteral("query"), s); } break; case ISBN: { QString s = request().value; // limit to first isbn s = s.section(QLatin1Char(';'), 0, 0); // isbn13 search doesn't work? s = ISBNValidator::isbn10(s); // dashes don't work s.remove(QLatin1Char('-')); q.addQueryItem(QStringLiteral("query"), s); } break; case Keyword: q.addQueryItem(QStringLiteral("query"), request().value); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &IBSFetcher::slotComplete); } void IBSFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void IBSFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } QString s = Tellico::decodeHTML(data); // really specific regexp QRegExp itemRx(QLatin1String("class=\"item \">(.*)class=\"price")); itemRx.setMinimal(true); QRegExp titleRx(QLatin1String("
    \\s*(.*)
    ")); titleRx.setMinimal(true); QRegExp yearRx(QLatin1String("(.*)")); tagRx.setMinimal(true); QString url, title, year; for(int pos = itemRx.indexIn(s); m_started && pos > -1; pos = itemRx.indexIn(s, pos+itemRx.matchedLength())) { QString s = itemRx.cap(1); if(s.contains(titleRx)) { url = titleRx.cap(1); title = titleRx.cap(2).remove(tagRx).simplified(); } if(s.contains(yearRx)) { year = yearRx.cap(1).remove(tagRx).simplified(); } if(!url.isEmpty() && !title.isEmpty()) { // the url probable contains & so be careful QUrl u = m_job->url(); u = u.resolved(QUrl(url.replace(QLatin1String("&"), QLatin1String("&")))); FetchResult* r = new FetchResult(Fetcher::Ptr(this), title, year); m_matches.insert(r->uid, u); emit signalResultFound(r); } } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; stop(); } Tellico::Data::EntryPtr IBSFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } QUrl url = m_matches[uid_]; if(url.isEmpty()) { myWarning() << "no url in map"; return Data::EntryPtr(); } QString results = Tellico::decodeHTML(FileHandler::readDataFile(url, true)); if(results.isEmpty()) { myDebug() << "no text results"; return Data::EntryPtr(); } // myDebug() << url.url(); #if 0 myWarning() << "Remove debug from ibsfetcher.cpp"; QFile f(QLatin1String("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << results; } f.close(); #endif entry = parseEntry(results); if(!entry) { myDebug() << "error in processing entry"; return Data::EntryPtr(); } m_entries.insert(uid_, entry); // keep for later return entry; } Tellico::Data::EntryPtr IBSFetcher::parseEntry(const QString& str_) { QRegExp jsonRx(QLatin1String("